ProfRick commited on
Commit
59c4057
·
verified ·
1 Parent(s): 29c79ee

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +447 -0
app.py ADDED
@@ -0,0 +1,447 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import matplotlib.pyplot as plt
3
+ import numpy as np
4
+ from io import BytesIO
5
+ from PIL import Image
6
+
7
+ # ---------- Transcript-authentic steps ----------
8
+ STEPS = [
9
+ dict(
10
+ title="Step 1: Motor neuron → NMJ",
11
+ where=("A motor neuron goes directly from the spinal cord to the skeletal muscle. "
12
+ "The action potential travels down the axon to the axon terminal, calcium channels open, "
13
+ "and acetylcholine is released into the synapse."),
14
+ question="When the motor neuron releases acetylcholine, what happens next?",
15
+ options=[
16
+ "The muscle relaxes immediately.",
17
+ "Acetylcholine moves across the synapse and binds to receptors on the muscle cell membrane.",
18
+ "Calcium leaves the muscle fiber."
19
+ ],
20
+ correct=1,
21
+ visual="neuron"
22
+ ),
23
+ dict(
24
+ title="Step 2: Motor end plate (ligand-gated channels)",
25
+ where=("Acetylcholine moves to the motor end plate and binds nicotinic acetylcholine receptors. "
26
+ "These are ligand-gated channels that open when acetylcholine binds."),
27
+ question="When acetylcholine binds to its receptor at the motor end plate, which ion moves into the muscle cell?",
28
+ options=[
29
+ "Sodium moves into the cell.",
30
+ "Potassium moves into the cell.",
31
+ "Calcium leaves the sarcoplasmic reticulum."
32
+ ],
33
+ correct=0,
34
+ visual="nmj"
35
+ ),
36
+ dict(
37
+ title="Step 3: Threshold → action potential on sarcolemma",
38
+ where=("Sodium entry changes the voltage until threshold potential is reached. "
39
+ "Voltage-gated channels open and an action potential travels along the sarcolemma."),
40
+ question="What allows the electrical signal to travel quickly across the muscle cell membrane?",
41
+ options=[
42
+ "Voltage-gated sodium channels opening along the sarcolemma.",
43
+ "Continuous acetylcholine release.",
44
+ "ATP from mitochondria."
45
+ ],
46
+ correct=0,
47
+ visual="sarcolemma"
48
+ ),
49
+ dict(
50
+ title="Step 4: T-tubule voltage sensing (DHP)",
51
+ where=("The sarcolemma dives into the cell as the T-tubule. "
52
+ "When the action potential reaches this area, the DHP receptor senses the voltage change."),
53
+ question="When the DHP receptor senses the voltage change, what does it do?",
54
+ options=[
55
+ "It moves the ryanidine receptor so calcium can leave the sarcoplasmic reticulum.",
56
+ "It brings more sodium into the cell.",
57
+ "It breaks down ATP."
58
+ ],
59
+ correct=0,
60
+ visual="t_tubule"
61
+ ),
62
+ dict(
63
+ title="Step 5: Calcium leaves the SR",
64
+ where=("The ryanidine receptor opens. Calcium moves out of the sarcoplasmic reticulum into the cytoplasm "
65
+ "following a concentration gradient (high in SR → lower in cytoplasm)."),
66
+ question="Why does calcium move out of the sarcoplasmic reticulum?",
67
+ options=[
68
+ "There is a high concentration of calcium inside the SR and a lower concentration in the cytoplasm.",
69
+ "Calcium is pushed out by sodium.",
70
+ "It is actively pumped out using ATP."
71
+ ],
72
+ correct=0,
73
+ visual="sr_release"
74
+ ),
75
+ dict(
76
+ title="Step 6: Troponin → tropomyosin moves",
77
+ where=("Once calcium is in the sarcoplasm, it binds to troponin, which causes tropomyosin to move away "
78
+ "from the binding sites on actin."),
79
+ question="What is exposed when tropomyosin moves?",
80
+ options=[
81
+ "The myosin binding sites on actin.",
82
+ "The ATP-binding sites on myosin.",
83
+ "The calcium pumps on the sarcoplasmic reticulum."
84
+ ],
85
+ correct=0,
86
+ visual="thin_filament"
87
+ ),
88
+ dict(
89
+ title="Step 7: Cross-bridge cycling (ATP’s role)",
90
+ where=("Myosin binds to actin. ATP causes detachment; ATP hydrolysis re-cocks the myosin head. "
91
+ "Without ATP, myosin stays attached and the muscle is stiff."),
92
+ question="If there is no ATP available, what happens?",
93
+ options=[
94
+ "The myosin remains attached to actin, causing stiffness.",
95
+ "The muscle continues to contract rapidly.",
96
+ "The SR releases more calcium."
97
+ ],
98
+ correct=0,
99
+ visual="crossbridge"
100
+ ),
101
+ dict(
102
+ title="Step 8: Relaxation",
103
+ where=("When the excitatory signal stops, acetylcholine esterase breaks down acetylcholine. "
104
+ "Calcium is pumped back into the sarcoplasmic reticulum, and tropomyosin moves back over the binding sites."),
105
+ question="What two actions cause relaxation?",
106
+ options=[
107
+ "Acetylcholine breakdown and calcium re-uptake into the sarcoplasmic reticulum.",
108
+ "More acetylcholine release and ATP depletion.",
109
+ "Sodium leaving the muscle fiber."
110
+ ],
111
+ correct=0,
112
+ visual="relax"
113
+ ),
114
+ ]
115
+
116
+ NODES = [
117
+ "ACh released at NMJ",
118
+ "ACh binds nicotinic receptor",
119
+ "Na⁺ entry → threshold → sarcolemma AP",
120
+ "T-tubule DHP senses voltage",
121
+ "RYR opens; Ca²⁺ leaves SR",
122
+ "Ca²⁺ binds troponin; tropomyosin moves",
123
+ "Cross-bridge cycling (ATP present)",
124
+ "Relaxation: AChE + SERCA"
125
+ ]
126
+
127
+ EDGES = {
128
+ "ACh released at NMJ": ["ACh binds nicotinic receptor"],
129
+ "ACh binds nicotinic receptor": ["Na⁺ entry → threshold → sarcolemma AP"],
130
+ "Na⁺ entry → threshold → sarcolemma AP": ["T-tubule DHP senses voltage"],
131
+ "T-tubule DHP senses voltage": ["RYR opens; Ca²⁺ leaves SR"],
132
+ "RYR opens; Ca²⁺ leaves SR": ["Ca²⁺ binds troponin; tropomyosin moves"],
133
+ "Ca²⁺ binds troponin; tropomyosin moves": ["Cross-bridge cycling (ATP present)"],
134
+ "Cross-bridge cycling (ATP present)": ["Relaxation: AChE + SERCA"],
135
+ "Relaxation: AChE + SERCA": []
136
+ }
137
+
138
+ # ---------- visuals (return numpy arrays; robust on Spaces) ----------
139
+ def draw_visual(kind: str) -> np.ndarray:
140
+ fig, ax = plt.subplots(figsize=(6, 3))
141
+ ax.axis("off")
142
+
143
+ def rect(x, y, w, h, fill=False):
144
+ ax.add_patch(plt.Rectangle((x,y), w, h, fill=fill))
145
+ def arrow(x, y, dx, dy):
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.05, 0.5, "Motor neuron → axon terminal (NMJ)\nACh released", ha="left", va="center")
154
+ arrow(0.35, 0.5, 0.45, 0)
155
+ circle(0.86, 0.5, 0.06)
156
+ txt(0.86, 0.5, "Terminal")
157
+ elif kind == "nmj":
158
+ rect(0.1, 0.35, 0.25, 0.3, fill=False)
159
+ txt(0.225, 0.66, "Presynaptic\nterminal")
160
+ txt(0.46, 0.50, "Synaptic cleft")
161
+ rect(0.62, 0.35, 0.28, 0.06, fill=True)
162
+ txt(0.76, 0.47, "Motor end plate\n(nicotinic AChR)")
163
+ arrow(0.35, 0.5, 0.22, 0)
164
+ txt(0.46, 0.56, "ACh →")
165
+ elif kind == "sarcolemma":
166
+ rect(0.1, 0.2, 0.8, 0.12, fill=True)
167
+ txt(0.5, 0.38, "Sarcolemma\n(voltage-gated Na⁺ open → AP)")
168
+ elif kind == "t_tubule":
169
+ rect(0.45, 0.1, 0.1, 0.8, fill=False)
170
+ txt(0.5, 0.93, "T-tubule")
171
+ rect(0.35, 0.1, 0.3, 0.1, fill=False)
172
+ txt(0.5, 0.06, "SR (RYR)")
173
+ txt(0.5, 0.5, "DHP senses voltage")
174
+ elif kind == "sr_release":
175
+ rect(0.2, 0.65, 0.6, 0.15, fill=False)
176
+ txt(0.5, 0.8, "SR: high [Ca²⁺]")
177
+ rect(0.2, 0.2, 0.6, 0.15, fill=False)
178
+ txt(0.5, 0.16, "Cytoplasm: lower [Ca²⁺]")
179
+ for x in [0.3, 0.45, 0.6]:
180
+ arrow(x, 0.65, 0, -0.25)
181
+ txt(0.5, 0.48, "Ca²⁺ follows concentration gradient")
182
+ elif kind == "thin_filament":
183
+ rect(0.1, 0.45, 0.8, 0.1, fill=True)
184
+ txt(0.5, 0.65, "Actin (thin filament)")
185
+ txt(0.5, 0.3, "Ca²⁺ binds troponin →\nTropomyosin moves")
186
+ elif kind == "crossbridge":
187
+ rect(0.1, 0.55, 0.8, 0.05, fill=True)
188
+ txt(0.5, 0.68, "Actin")
189
+ rect(0.1, 0.35, 0.8, 0.05, fill=True)
190
+ txt(0.5, 0.28, "Myosin")
191
+ txt(0.5, 0.47, "ATP binds → detachment\nATP hydrolysis → re-cock")
192
+ elif kind == "relax":
193
+ rect(0.2, 0.65, 0.6, 0.15, fill=False)
194
+ txt(0.5, 0.8, "SR")
195
+ rect(0.2, 0.2, 0.6, 0.15, fill=False)
196
+ for x in [0.3, 0.45, 0.6]:
197
+ arrow(x, 0.35, 0, 0.25)
198
+ txt(0.5, 0.55, "Ca²⁺ pumped back (ATP-dependent)")
199
+ txt(0.5, 0.12, "AChE breaks down acetylcholine")
200
+
201
+ buf = BytesIO()
202
+ plt.tight_layout()
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
+ # ---------- Step Trainer logic ----------
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 clear
220
+ i, # state
221
+ i) # progress
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
227
+ if idx == s["correct"]:
228
+ if i < len(STEPS)-1:
229
+ i += 1
230
+ fb = "✅ **Correct. Advancing…**"
231
+ else:
232
+ fb = "✅ **Done. Relaxation complete.**"
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
+ # ---------- Failure-Point ----------
244
+ def failure_check(fails, guess):
245
+ failed_idx = sorted([NODES.index(f) for f in (fails or [])]) if fails else []
246
+ lines = []
247
+ if failed_idx:
248
+ stop = failed_idx[0]
249
+ for j, lab in enumerate(NODES):
250
+ ok = j < stop
251
+ lines.append(("✅ " if ok else "⛔ ") + lab)
252
+ if not ok:
253
+ break
254
+ fb = "✅ **Correct: first failed step located.**" if (guess and NODES.index(guess)==stop) else "❌ **Not quite—identify the FIRST failed step.**"
255
+ else:
256
+ lines = ["✅ " + l for l in NODES]
257
+ lines.append("(No failures set — full propagation to relaxation.)")
258
+ fb = "No failures toggled."
259
+ return "```\n" + "\n".join(lines) + "\n```", fb
260
+
261
+ # ---------- Sandbox ----------
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)
265
+ dhp_ok = ap_prob
266
+ ca_drive = max(0.0, (Ca_sr - Ca_cyto) / max(1.0, Ca_sr))
267
+ ca_rel = dhp_ok * ca_drive
268
+ xbridges = min(1.0, ca_rel * (0.5 + 0.5*min(1.0, ATP)))
269
+ relax_ok = min(1.0, ATP) * 0.7 + (1.0 - ca_rel) * 0.3
270
+ return dict(ap_prob=ap_prob, ca_release=ca_rel, crossbridge=xbridges, relax_ok=relax_ok)
271
+
272
+ 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
+ ax.set_title("Predicted behaviors (0–1)")
279
+ buf = BytesIO(); fig.tight_layout(); fig.savefig(buf, format="png", bbox_inches="tight"); plt.close(fig)
280
+ buf.seek(0); img = Image.open(buf).convert("RGB")
281
+ return np.array(img)
282
+
283
+ # ---------- Causality Builder ----------
284
+ def chain_add(chain, pick):
285
+ import json
286
+ chain = json.loads(chain)
287
+ remaining = [n for n in NODES if n not in chain]
288
+ if not remaining:
289
+ return (gr.update(value=chain, interactive=False),
290
+ "Chain complete.",
291
+ gr.update(choices=[], interactive=False),
292
+ " → ".join(chain))
293
+ if pick is None:
294
+ return (gr.update(value=chain),
295
+ "Pick a next event.",
296
+ gr.update(),
297
+ " → ".join(chain))
298
+ ok = pick in EDGES.get(chain[-1], [])
299
+ if ok:
300
+ chain.append(pick)
301
+ remaining = [n for n in NODES if n not in chain]
302
+ msg = "✅ **Link accepted.**"
303
+ else:
304
+ msg = "❌ **That event doesn’t logically follow. Try a different next step.**"
305
+ return (gr.update(value=chain),
306
+ msg,
307
+ gr.update(choices=remaining, value=(remaining[0] if remaining else None), interactive=bool(remaining)),
308
+ " → ".join(chain))
309
+
310
+ def chain_reset():
311
+ import json
312
+ chain = [NODES[0]]
313
+ remaining = [n for n in NODES if n not in chain]
314
+ return (gr.update(value=chain),
315
+ "Reset.",
316
+ gr.update(choices=remaining, value=remaining[0] if remaining else None, interactive=bool(remaining)),
317
+ " → ".join(chain))
318
+
319
+ # ---------- Fatigue Lab ----------
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):
323
+ cons = load*0.4 + serca_load*0.25
324
+ prod = aerobic*0.2 + anaerobic*0.15
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
+ # ---------- Passport Analyzer ----------
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"],
339
+ "Structure–function": ["structure","function","troponin","tropomyosin","binding site","receptor opens"],
340
+ "Energy flow": ["ATP","ADP","Pi","hydrolysis","energy"],
341
+ "Interdependence": ["depends","linked","together","if/then","cascade","pathway"]
342
+ }
343
+ 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
+ buf = BytesIO(); fig.tight_layout(); fig.savefig(buf, format="png", bbox_inches="tight"); plt.close(fig)
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
+ # ---------- Build UI ----------
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"):
362
+ step_state = gr.State(0)
363
+ title = gr.Markdown()
364
+ where = gr.Markdown()
365
+ img = gr.Image(label="Where are we?")
366
+ q = gr.Markdown()
367
+ choice = gr.Radio(choices=[], label="Predict what happens next")
368
+ submit = gr.Button("Submit", variant="primary")
369
+ restart = gr.Button("Restart (Step 1)")
370
+ fb = gr.Markdown()
371
+ prog = gr.Slider(0, len(STEPS)-1, value=0, step=1, interactive=False, label="Progress")
372
+
373
+ # initialize on load (avoid setting .value directly)
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], [title, where, img, q, choice, fb, step_state, prog])
379
+ restart.click(restart_step, [step_state], [title, where, img, q, choice, fb, step_state, prog])
380
+
381
+ # Failure-Point
382
+ with gr.Tab("Failure-Point"):
383
+ gr.Markdown("Toggle failures and diagnose the **first** failed step.")
384
+ fails = gr.CheckboxGroup(choices=NODES, label="Failures")
385
+ guess = gr.Dropdown(choices=NODES, label="Your diagnosis")
386
+ check = gr.Button("Test & Check", variant="primary")
387
+ log = gr.Markdown()
388
+ verdict = gr.Markdown()
389
+ check.click(failure_check, [fails, guess], [log, verdict])
390
+
391
+ # Sandbox
392
+ with gr.Tab("Sandbox"):
393
+ gr.Markdown("Adjust gradients and ATP; observe predicted behaviors (heuristic).")
394
+ Na_out = gr.Slider(10, 160, value=140, step=1, label='[Na⁺] outside')
395
+ Na_in = gr.Slider(0, 50, value=15, step=1, label='[Na⁺] inside')
396
+ Ca_sr = gr.Slider(0.1, 10.0, value=3.0, step=0.1, label='[Ca²⁺] SR')
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)
404
+
405
+ # Causality
406
+ with gr.Tab("Causality"):
407
+ gr.Markdown("Build a valid chain; logic is checked at each link.")
408
+ import json
409
+ chain_state = gr.State(json.dumps([NODES[0]]))
410
+ chain_text = gr.Markdown(" → ".join([NODES[0]]))
411
+ next_pick = gr.Dropdown(choices=[n for n in NODES if n != NODES[0]], label="Next event")
412
+ add = gr.Button("Add link", variant="primary")
413
+ reset = gr.Button("Reset")
414
+ fb2 = gr.Markdown()
415
+ add.click(chain_add, [chain_state, next_pick], [chain_state, fb2, next_pick, chain_text])
416
+ reset.click(chain_reset, [], [chain_state, fb2, next_pick, chain_text])
417
+
418
+ # Fatigue
419
+ with gr.Tab("Fatigue"):
420
+ gr.Markdown("Adjust ATP supply/demand; see ATP curve and rigor fraction.")
421
+ aerobic = gr.Slider(0,1,value=0.4,step=0.01,label="Aerobic supply")
422
+ anaer = gr.Slider(0,1,value=0.3,step=0.01,label="Anaerobic supply")
423
+ work = gr.Slider(0,1,value=0.6,step=0.01,label="Mechanical load")
424
+ serca = gr.Slider(0,1,value=0.4,step=0.01,label="SERCA load")
425
+ dur = gr.Slider(10,180,value=90,step=1,label="Duration (s)")
426
+ ftg_img = gr.Image(label="ATP & Rigor")
427
+ ftg_txt = gr.Markdown()
428
+
429
+ def ftg_update(dur_val, aer, anr, load, sl):
430
+ return simulate_fatigue(dur_val, 0.5, 1.0, aer, anr, load, sl)
431
+
432
+ for w in [aerobic, anaer, work, serca, dur]:
433
+ w.change(ftg_update, [dur, aerobic, anaer, work, serca], [ftg_img, ftg_txt])
434
+
435
+ img0, txt0 = simulate_fatigue(90, 0.5, 1.0, 0.4, 0.3, 0.6, 0.4)
436
+ ftg_img.value, ftg_txt.value = img0, txt0
437
+
438
+ # Passport
439
+ with gr.Tab("Passport"):
440
+ gr.Markdown("Paste your notes; see Core Concept emphasis.")
441
+ ta = gr.Textbox(lines=8, label="Notes / reflection")
442
+ pass_img = gr.Image(label="Concept counts")
443
+ pass_txt = gr.Markdown()
444
+ run = gr.Button("Analyze", variant="primary")
445
+ run.click(passport_analyze, [ta], [pass_img, pass_txt])
446
+
447
+ demo.launch()