Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -3,8 +3,10 @@ import matplotlib.pyplot as plt
|
|
| 3 |
import numpy as np
|
| 4 |
from io import BytesIO
|
| 5 |
from PIL import Image
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
#
|
| 8 |
STEPS = [
|
| 9 |
dict(
|
| 10 |
title="Step 1: Motor neuron → NMJ",
|
|
@@ -135,92 +137,210 @@ EDGES = {
|
|
| 135 |
"Relaxation: AChE + SERCA": []
|
| 136 |
}
|
| 137 |
|
| 138 |
-
#
|
| 139 |
-
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
ax.axis("off")
|
| 142 |
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
ax.arrow(x, y, dx, dy, head_width=0.03, head_length=0.02, length_includes_head=True)
|
| 147 |
-
def circle(cx, cy, r):
|
| 148 |
-
ax.add_patch(plt.Circle((cx, cy), r, fill=False))
|
| 149 |
-
def txt(x, y, t):
|
| 150 |
-
ax.text(x, y, t, ha="center", va="center")
|
| 151 |
|
| 152 |
if kind == "neuron":
|
| 153 |
-
ax.text(0.
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
elif kind == "nmj":
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
elif kind == "sarcolemma":
|
| 166 |
-
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
elif kind == "t_tubule":
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
elif kind == "sr_release":
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
for x in
|
| 180 |
-
|
| 181 |
-
|
|
|
|
| 182 |
elif kind == "thin_filament":
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
elif kind == "crossbridge":
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
elif kind == "relax":
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
for x in
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
|
|
|
| 200 |
|
|
|
|
| 201 |
buf = BytesIO()
|
| 202 |
-
|
| 203 |
-
fig.savefig(buf, format="png", bbox_inches="tight")
|
| 204 |
plt.close(fig)
|
| 205 |
buf.seek(0)
|
| 206 |
img = Image.open(buf).convert("RGB")
|
| 207 |
return np.array(img)
|
| 208 |
|
| 209 |
-
#
|
| 210 |
-
def render_step(i:int):
|
| 211 |
i = int(i)
|
| 212 |
s = STEPS[i]
|
| 213 |
-
img = draw_visual(s["visual"])
|
| 214 |
return (f"### {s['title']}",
|
| 215 |
s["where"],
|
| 216 |
img,
|
| 217 |
f"**{s['question']}**",
|
| 218 |
gr.update(choices=s["options"], value=s["options"][0]),
|
| 219 |
-
"", # feedback
|
| 220 |
i, # state
|
| 221 |
-
i)
|
| 222 |
|
| 223 |
-
def submit_step(i:int, picked:str):
|
| 224 |
i = int(i)
|
| 225 |
s = STEPS[i]
|
| 226 |
idx = s["options"].index(picked) if picked in s["options"] else -1
|
|
@@ -233,14 +353,14 @@ def submit_step(i:int, picked:str):
|
|
| 233 |
else:
|
| 234 |
i = 0
|
| 235 |
fb = "❌ **Incorrect. Returning to Step 1.**"
|
| 236 |
-
title, where, img, q, choices, _, _, _ = render_step(i)
|
| 237 |
return title, where, img, q, choices, fb, i, i
|
| 238 |
|
| 239 |
-
def restart_step(_i:int):
|
| 240 |
-
title, where, img, q, choices, _, i, p = render_step(0)
|
| 241 |
return title, where, img, q, choices, "Restarted.", i, p
|
| 242 |
|
| 243 |
-
#
|
| 244 |
def failure_check(fails, guess):
|
| 245 |
failed_idx = sorted([NODES.index(f) for f in (fails or [])]) if fails else []
|
| 246 |
lines = []
|
|
@@ -258,7 +378,7 @@ def failure_check(fails, guess):
|
|
| 258 |
fb = "No failures toggled."
|
| 259 |
return "```\n" + "\n".join(lines) + "\n```", fb
|
| 260 |
|
| 261 |
-
#
|
| 262 |
def sandbox_metrics(Na_out, Na_in, Ca_sr, Ca_cyto, ATP):
|
| 263 |
na_drive = max(0.0, (Na_out - Na_in) / max(1.0, Na_out))
|
| 264 |
ap_prob = min(1.0, na_drive * 1.5)
|
|
@@ -273,14 +393,15 @@ def sandbox_plot(Na_out, Na_in, Ca_sr, Ca_cyto, ATP):
|
|
| 273 |
vals = sandbox_metrics(Na_out, Na_in, Ca_sr, Ca_cyto, ATP)
|
| 274 |
fig, ax = plt.subplots(figsize=(6,3))
|
| 275 |
keys = ["ap_prob","ca_release","crossbridge","relax_ok"]
|
| 276 |
-
ax.bar(keys, [vals[k] for k in keys])
|
| 277 |
-
ax.set_ylim(0,1)
|
| 278 |
-
|
| 279 |
-
|
|
|
|
| 280 |
buf.seek(0); img = Image.open(buf).convert("RGB")
|
| 281 |
return np.array(img)
|
| 282 |
|
| 283 |
-
#
|
| 284 |
def chain_add(chain, pick):
|
| 285 |
import json
|
| 286 |
chain = json.loads(chain)
|
|
@@ -316,7 +437,7 @@ def chain_reset():
|
|
| 316 |
gr.update(choices=remaining, value=remaining[0] if remaining else None, interactive=bool(remaining)),
|
| 317 |
" → ".join(chain))
|
| 318 |
|
| 319 |
-
#
|
| 320 |
def simulate_fatigue(tmax=60, dt=0.5, atp_init=1.0, aerobic=0.3, anaerobic=0.2, load=0.5, serca_load=0.3):
|
| 321 |
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)
|
| 322 |
for i in range(1,n):
|
|
@@ -325,14 +446,14 @@ def simulate_fatigue(tmax=60, dt=0.5, atp_init=1.0, aerobic=0.3, anaerobic=0.2,
|
|
| 325 |
atp[i] = np.clip(atp[i-1] + dt*(prod - cons), 0, 1.2)
|
| 326 |
rigor[i] = atp[i] < 0.1
|
| 327 |
fig, ax = plt.subplots(1,2, figsize=(8,3))
|
| 328 |
-
ax[0].plot(t, atp); ax[0].set_xlabel("time (s)"); ax[0].set_ylabel("ATP (a.u.)"); ax[0].set_title("ATP dynamics")
|
| 329 |
-
ax[1].bar(["rigor fraction"], [rigor.mean()]); ax[1].set_ylim(0,1); ax[1].set_title("Rigor")
|
| 330 |
-
buf = BytesIO(); fig.tight_layout(); fig.savefig(buf, format="png", bbox_inches="tight"); plt.close(fig)
|
| 331 |
buf.seek(0); img = Image.open(buf).convert("RGB")
|
| 332 |
msg = "Low ATP periods → stiffness (myosin remains attached)." if rigor.mean()>0 else "No stiffness expected (ATP maintained)."
|
| 333 |
return np.array(img), f"Estimated rigor fraction: {rigor.mean():.2f}\n{msg}"
|
| 334 |
|
| 335 |
-
#
|
| 336 |
CORE_CONCEPTS = {
|
| 337 |
"Flow down gradients": ["gradient","concentration","moves from high to low","diffuse","diffusion"],
|
| 338 |
"Cell-to-cell communication": ["neurotransmitter","acetylcholine","receptor","synapse","binds"],
|
|
@@ -344,18 +465,23 @@ def passport_analyze(text):
|
|
| 344 |
t = (text or "").lower()
|
| 345 |
counts = {k: sum(t.count(w) for w in ws) for k,ws in CORE_CONCEPTS.items()}
|
| 346 |
keys = list(counts.keys()); vals = [counts[k] for k in keys]
|
| 347 |
-
fig, ax = plt.subplots(figsize=(6,3)); ax.bar(keys, vals); ax.set_title("Core Concept mentions"); ax.tick_params(axis='x', rotation=30)
|
| 348 |
-
|
|
|
|
|
|
|
| 349 |
buf.seek(0); img = Image.open(buf).convert("RGB")
|
| 350 |
weak = [k for k in keys if counts[k]==0]
|
| 351 |
note = ("Consider adding explicit references to: " + ", ".join(weak)) if weak else "Balanced coverage detected."
|
| 352 |
import json
|
| 353 |
return np.array(img), json.dumps(counts, indent=2) + "\n\n" + note
|
| 354 |
|
| 355 |
-
#
|
| 356 |
with gr.Blocks(title="EC Coupling Suite") as demo:
|
| 357 |
gr.Markdown("# EC Coupling Learning Suite (Transcript Language)")
|
| 358 |
|
|
|
|
|
|
|
|
|
|
| 359 |
with gr.Tabs():
|
| 360 |
# Step Trainer
|
| 361 |
with gr.Tab("Step Trainer"):
|
|
@@ -370,13 +496,13 @@ with gr.Blocks(title="EC Coupling Suite") as demo:
|
|
| 370 |
fb = gr.Markdown()
|
| 371 |
prog = gr.Slider(0, len(STEPS)-1, value=0, step=1, interactive=False, label="Progress")
|
| 372 |
|
| 373 |
-
|
| 374 |
-
demo.load(fn=lambda: render_step(0),
|
| 375 |
-
inputs=None,
|
| 376 |
outputs=[title, where, img, q, choice, fb, step_state, prog])
|
| 377 |
|
| 378 |
-
submit.click(submit_step, [step_state, choice
|
| 379 |
-
|
|
|
|
|
|
|
| 380 |
|
| 381 |
# Failure-Point
|
| 382 |
with gr.Tab("Failure-Point"):
|
|
@@ -397,7 +523,6 @@ with gr.Blocks(title="EC Coupling Suite") as demo:
|
|
| 397 |
Ca_c = gr.Slider(0.0, 1.0, value=0.1, step=0.01, label='[Ca²⁺] cytoplasm')
|
| 398 |
ATP = gr.Slider(0.0, 1.0, value=0.8, step=0.01, label='ATP (0–1)')
|
| 399 |
sb_img = gr.Image(label="Predicted bars")
|
| 400 |
-
|
| 401 |
for w in [Na_out, Na_in, Ca_sr, Ca_c, ATP]:
|
| 402 |
w.change(sandbox_plot, [Na_out, Na_in, Ca_sr, Ca_c, ATP], [sb_img])
|
| 403 |
sb_img.value = sandbox_plot(Na_out.value, Na_in.value, Ca_sr.value, Ca_c.value, ATP.value)
|
|
|
|
| 3 |
import numpy as np
|
| 4 |
from io import BytesIO
|
| 5 |
from PIL import Image
|
| 6 |
+
from matplotlib.patches import Rectangle, Circle, FancyArrowPatch, PathPatch
|
| 7 |
+
from matplotlib.path import Path
|
| 8 |
|
| 9 |
+
# ================== Content (transcript language) ==================
|
| 10 |
STEPS = [
|
| 11 |
dict(
|
| 12 |
title="Step 1: Motor neuron → NMJ",
|
|
|
|
| 137 |
"Relaxation: AChE + SERCA": []
|
| 138 |
}
|
| 139 |
|
| 140 |
+
# ================== Visual style helpers (HD schematics) ==================
|
| 141 |
+
PALETTE = {
|
| 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 |
+
|
| 154 |
+
def _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 |
+
|
| 158 |
+
def _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 |
+
|
| 162 |
+
def _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 |
+
|
| 166 |
+
def _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 |
+
|
| 171 |
+
def _nicotinic_receptor(ax, x=0.8, y=0.5, w=0.06, h=0.04):
|
| 172 |
+
# dimer-like shapes
|
| 173 |
+
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 |
+
|
| 177 |
+
def _channel(ax, x, y, open_state=True, label=None):
|
| 178 |
+
h = 0.06
|
| 179 |
+
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 |
+
|
| 188 |
+
def _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_pos
|
| 200 |
+
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 |
+
|
| 204 |
+
def _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 |
+
|
| 209 |
+
def _legend(ax, items):
|
| 210 |
+
# items: list of (color, label)
|
| 211 |
+
x, y = 0.05, 0.05
|
| 212 |
+
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 |
+
|
| 216 |
+
def draw_visual(kind: str, detail: str = "HD") -> np.ndarray:
|
| 217 |
+
"""
|
| 218 |
+
detail: 'Basic' or 'HD'
|
| 219 |
+
"""
|
| 220 |
+
# Larger canvas for HD; use antialiasing and facecolors
|
| 221 |
+
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 views
|
| 228 |
+
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 |
+
# Axon
|
| 234 |
+
ax.add_line(plt.Line2D([0.06, 0.38], [0.5, 0.5], color=PALETTE["membrane"], linewidth=5))
|
| 235 |
+
# Vesicles at terminal
|
| 236 |
+
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 membrane
|
| 239 |
+
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 receptor
|
| 248 |
+
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 influx
|
| 259 |
+
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 arrow
|
| 275 |
+
_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 -> cytosol
|
| 282 |
+
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 cable
|
| 289 |
+
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 troponin
|
| 294 |
+
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 binding
|
| 305 |
+
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 SR
|
| 314 |
+
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 array
|
| 321 |
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) ==================
|
| 330 |
+
def 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 |
+
"", # feedback
|
| 340 |
i, # state
|
| 341 |
+
i)
|
| 342 |
|
| 343 |
+
def 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 -1
|
|
|
|
| 353 |
else:
|
| 354 |
i = 0
|
| 355 |
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, i
|
| 358 |
|
| 359 |
+
def 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, p
|
| 362 |
|
| 363 |
+
# ================== Failure-Point ==================
|
| 364 |
def failure_check(fails, guess):
|
| 365 |
failed_idx = sorted([NODES.index(f) for f in (fails or [])]) if fails else []
|
| 366 |
lines = []
|
|
|
|
| 378 |
fb = "No failures toggled."
|
| 379 |
return "```\n" + "\n".join(lines) + "\n```", fb
|
| 380 |
|
| 381 |
+
# ================== Sandbox ==================
|
| 382 |
def 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)
|
|
|
|
| 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 ==================
|
| 405 |
def chain_add(chain, pick):
|
| 406 |
import json
|
| 407 |
chain = json.loads(chain)
|
|
|
|
| 437 |
gr.update(choices=remaining, value=remaining[0] if remaining else None, interactive=bool(remaining)),
|
| 438 |
" → ".join(chain))
|
| 439 |
|
| 440 |
+
# ================== Fatigue Lab ==================
|
| 441 |
def 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):
|
|
|
|
| 446 |
atp[i] = np.clip(atp[i-1] + dt*(prod - cons), 0, 1.2)
|
| 447 |
rigor[i] = atp[i] < 0.1
|
| 448 |
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 ==================
|
| 457 |
CORE_CONCEPTS = {
|
| 458 |
"Flow down gradients": ["gradient","concentration","moves from high to low","diffuse","diffusion"],
|
| 459 |
"Cell-to-cell communication": ["neurotransmitter","acetylcholine","receptor","synapse","binds"],
|
|
|
|
| 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 json
|
| 476 |
return np.array(img), json.dumps(counts, indent=2) + "\n\n" + note
|
| 477 |
|
| 478 |
+
# ================== UI ==================
|
| 479 |
with 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 Trainer
|
| 487 |
with gr.Tab("Step Trainer"):
|
|
|
|
| 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-Point
|
| 508 |
with gr.Tab("Failure-Point"):
|
|
|
|
| 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)
|