ProfRick commited on
Commit
6b49fc5
·
verified ·
1 Parent(s): 6253dbf

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +229 -0
app.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import streamlit as st
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+ from matplotlib.patches import Circle, Arc, Rectangle
6
+ from matplotlib.lines import Line2D
7
+
8
+ st.set_page_config(page_title="Flow Down Gradients — Simple Explorer", layout="wide")
9
+
10
+ # ---------------- Pressure presets (mmHg) ----------------
11
+ PRESSURE_PRESETS = {
12
+ "Demo Preset": {
13
+ "cap_o2": 52, "cap_co2": 49,
14
+ "alv_o2": 55, "alv_co2": 45,
15
+ "thoracic": 761,
16
+ },
17
+ "Preset 1": {
18
+ "cap_o2": 50, "cap_co2": 45,
19
+ "alv_o2": 100, "alv_co2": 40,
20
+ "thoracic": 763,
21
+ },
22
+ "Preset 2": {
23
+ "cap_o2": 60, "cap_co2": 45,
24
+ "alv_o2": 58, "alv_co2": 40,
25
+ "thoracic": 757,
26
+ },
27
+ "Preset 3": {
28
+ "cap_o2": 48, "cap_co2": 45,
29
+ "alv_o2": 53, "alv_co2": 8,
30
+ "thoracic": 768,
31
+ },
32
+ }
33
+
34
+ def get_pressure_preset_from_query():
35
+ qp = st.query_params
36
+ key = qp.get("preset") if "preset" in qp else None
37
+ if key in PRESSURE_PRESETS:
38
+ return key
39
+ return "Demo Preset"
40
+
41
+ if "pressure_preset" not in st.session_state:
42
+ st.session_state.pressure_preset = get_pressure_preset_from_query()
43
+
44
+ # ---------------- Action Potential presets ----------------
45
+ AP_PRESETS = {
46
+ "Preset 1": {"more_na": "Extracellular", "more_k": "Intracellular", "more_neg": "Intracellular"},
47
+ "Preset 2": {"more_na": "Intracellular", "more_k": "Extracellular", "more_neg": "Extracellular"},
48
+ "Preset 3": {"more_na": "Extracellular", "more_k": "Intracellular", "more_neg": "Extracellular"},
49
+ }
50
+
51
+ if "ap_preset" not in st.session_state:
52
+ st.session_state.ap_preset = "Preset 1"
53
+
54
+ # ---------------- UI ----------------
55
+ st.title("Flow Down Gradients — Simple Explorer")
56
+
57
+ tab1, tab2 = st.tabs([
58
+ "Pressure (Alveolus ↔ Capillary)",
59
+ "Concentration: Action Potential (extracellular ↔ intracellular)",
60
+ ])
61
+
62
+ # ---------------- Tab 1: Pressure ----------------
63
+ with tab1:
64
+ st.subheader("Pressure Gradients (mmHg)")
65
+ # Preset buttons with unique keys
66
+ bcols = st.columns(4)
67
+ for i, (name, col) in enumerate(zip(PRESSURE_PRESETS.keys(), bcols)):
68
+ if col.button(name, key=f"pressure_btn_{i}_{name}", use_container_width=True):
69
+ st.session_state.pressure_preset = name
70
+
71
+ # Current preset
72
+ preset_name = st.session_state.pressure_preset
73
+ p = PRESSURE_PRESETS[preset_name]
74
+
75
+ # Values
76
+ st.markdown(f"**Current preset:** {preset_name}")
77
+ st.markdown(
78
+ f"- **Intracapillary O₂:** {p['cap_o2']} mmHg \n"
79
+ f"- **Intracapillary CO₂:** {p['cap_co2']} mmHg \n"
80
+ f"- **Intraalveolar O₂:** {p['alv_o2']} mmHg \n"
81
+ f"- **Intraalveolar CO₂:** {p['alv_co2']} mmHg \n"
82
+ f"- **Thoracic pressure:** {p['thoracic']} mmHg"
83
+ )
84
+
85
+ # Diagram (enlarged, placed below values)
86
+ fig, ax = plt.subplots(figsize=(11.5, 5.6))
87
+ ax.set_xlim(0, 12); ax.set_ylim(0, 6); ax.axis("off")
88
+
89
+ # Alveolus: round sac
90
+ alve_center = (4.0, 3.0); alve_r = 2.1
91
+ alve = Circle(alve_center, alve_r, fill=False, lw=2)
92
+ ax.add_patch(alve)
93
+ ax.text(alve_center[0], alve_center[1] + alve_r + 0.35, "Alveolus", ha="center", fontsize=13)
94
+
95
+ # Capillary: thin vessel hugging right side (two arcs + short straight segments)
96
+ cap_outer = Arc((6.2, 3.0), 3.6, 3.6, angle=0, theta1=-65, theta2=65, lw=6, capstyle="round")
97
+ cap_inner = Arc((6.2, 3.0), 3.0, 3.0, angle=0, theta1=-65, theta2=65, lw=6, capstyle="round")
98
+ ax.add_patch(cap_outer); ax.add_patch(cap_inner)
99
+ # Straight ends to right
100
+ ax.plot([7.8, 11.0], [4.2, 4.2], lw=6, solid_capstyle="round")
101
+ ax.plot([7.8, 11.0], [1.8, 1.8], lw=6, solid_capstyle="round")
102
+ ax.text(11.2, 3.0, "Capillary", va="center", fontsize=13, rotation=90)
103
+
104
+ # Accessibility-friendly markers/colors
105
+ color_o2, marker_o2 = "tab:blue", "o" # O2 = blue circle
106
+ color_co2, marker_co2 = "tab:orange", "s" # CO2 = orange square
107
+
108
+ # Dot counts scaled from mmHg (cap at 150 points per region)
109
+ maxdots = 150
110
+ ao2 = int(np.clip(p["alv_o2"], 0, maxdots))
111
+ aco2 = int(np.clip(p["alv_co2"], 0, maxdots))
112
+ co2 = int(np.clip(p["cap_o2"], 0, maxdots))
113
+ cco2 = int(np.clip(p["cap_co2"], 0, maxdots))
114
+
115
+ # Alveolar dots: random inside circle
116
+ rngA = np.random.default_rng(101)
117
+ need = ao2 + aco2
118
+ if need > 0:
119
+ xs, ys = [], []
120
+ while len(xs) < need:
121
+ x = rngA.uniform(alve_center[0]-alve_r+0.12, alve_center[0]+alve_r-0.12)
122
+ y = rngA.uniform(alve_center[1]-alve_r+0.12, alve_center[1]+alve_r-0.12)
123
+ if (x-alve_center[0])**2 + (y-alve_center[1])**2 <= (alve_r-0.18)**2:
124
+ xs.append(x); ys.append(y)
125
+ xs = np.array(xs); ys = np.array(ys)
126
+ if ao2 > 0:
127
+ ax.scatter(xs[:ao2], ys[:ao2], s=36, c=color_o2, marker=marker_o2)
128
+ if aco2 > 0:
129
+ ax.scatter(xs[ao2:], ys[ao2:], s=36, c=color_co2, marker=marker_co2)
130
+
131
+ # Capillary dots: right rectangular segment for clarity
132
+ rngC = np.random.default_rng(202)
133
+ cx = rngC.uniform(8.2, 10.8, co2 + cco2)
134
+ cy = rngC.uniform(1.95, 4.05, co2 + cco2)
135
+ if co2 > 0:
136
+ ax.scatter(cx[:co2], cy[:co2], s=36, c=color_o2, marker=marker_o2)
137
+ if cco2 > 0:
138
+ ax.scatter(cx[co2:], cy[co2:], s=36, c=color_co2, marker=marker_co2)
139
+
140
+ # Clean, accessible legend using Line2D handles (not cramped)
141
+ legend_elements = [
142
+ Line2D([0], [0], marker=marker_o2, color='w', label='O₂ (Blue • Circle)',
143
+ markerfacecolor=color_o2, markersize=10),
144
+ Line2D([0], [0], marker=marker_co2, color='w', label='CO₂ (Orange • Square)',
145
+ markerfacecolor=color_co2, markersize=10),
146
+ ]
147
+ ax.legend(handles=legend_elements, loc='upper left', bbox_to_anchor=(0.02, 0.98), frameon=False)
148
+
149
+ # Stamp preset name
150
+ ax.text(0.98, 0.05, f"{preset_name}", transform=ax.transAxes, ha="right", va="bottom", fontsize=11, alpha=0.8)
151
+
152
+ st.pyplot(fig, clear_figure=True)
153
+
154
+ # ---------------- Tab 2: Action Potential (presets, no sliders) ----------------
155
+ with tab2:
156
+ st.subheader("Static starting conditions (no animation)")
157
+ # Preset buttons with unique keys
158
+ bcols = st.columns(3)
159
+ for i, (name, col) in enumerate(zip(AP_PRESETS.keys(), bcols)):
160
+ if col.button(name, key=f"ap_btn_{i}_{name}", use_container_width=True):
161
+ st.session_state.ap_preset = name
162
+
163
+ ap = AP_PRESETS[st.session_state.ap_preset]
164
+ st.markdown(f"**Current preset:** {st.session_state.ap_preset}")
165
+ st.markdown(
166
+ f"- More **Na⁺**: {ap['more_na']} \n"
167
+ f"- More **K⁺**: {ap['more_k']} \n"
168
+ f"- More **negative**: {ap['more_neg']}"
169
+ )
170
+
171
+ fig, ax = plt.subplots(figsize=(11.5, 5.0))
172
+ ax.set_xlim(0, 12); ax.set_ylim(0, 6); ax.axis("off")
173
+
174
+ # Compartments
175
+ ax.add_patch(Rectangle((0.8, 1.0), 4.5, 4.0, fill=False, lw=2)) # Extracellular
176
+ ax.add_patch(Rectangle((6.7, 1.0), 4.5, 4.0, fill=False, lw=2)) # Intracellular
177
+ ax.plot([6.2, 6.2], [1.0, 5.0], lw=8) # Membrane
178
+
179
+ ax.text(3.05, 5.25, "Extracellular", ha="center", fontsize=12)
180
+ ax.text(8.95, 5.25, "Intracellular", ha="center", fontsize=12)
181
+
182
+ # Polarity signs ABOVE labels
183
+ if ap["more_neg"] == "Intracellular":
184
+ ax.text(3.05, 5.85, "+", ha="center", va="center", fontsize=24, alpha=0.6)
185
+ ax.text(8.95, 5.85, "−", ha="center", va="center", fontsize=24, alpha=0.9)
186
+ else:
187
+ ax.text(3.05, 5.85, "−", ha="center", va="center", fontsize=24, alpha=0.9)
188
+ ax.text(8.95, 5.85, "+", ha="center", va="center", fontsize=24, alpha=0.6)
189
+
190
+ # Colors + shapes (accessibility): Na+ = blue circle, K+ = orange square
191
+ color_na, marker_na = "tab:blue", "o"
192
+ color_k, marker_k = "tab:orange", "s"
193
+
194
+ # Dot counts: base + extra on "more" side
195
+ base_amt = 12
196
+ extra_amt = 12
197
+ na_out = base_amt + (extra_amt if ap["more_na"] == "Extracellular" else 0)
198
+ na_in = base_amt + (extra_amt if ap["more_na"] == "Intracellular" else 0)
199
+ k_out = base_amt + (extra_amt if ap["more_k"] == "Extracellular" else 0)
200
+ k_in = base_amt + (extra_amt if ap["more_k"] == "Intracellular" else 0)
201
+
202
+ rng2 = np.random.default_rng(808)
203
+ xo = rng2.uniform(1.1, 5.0, na_out + k_out); yo = rng2.uniform(1.2, 4.8, na_out + k_out)
204
+ xi = rng2.uniform(7.0, 11.4, na_in + k_in ); yi = rng2.uniform(1.2, 4.8, na_in + k_in )
205
+
206
+ # Draw ions with fixed color+shape per type, regardless of side
207
+ if na_out > 0:
208
+ ax.scatter(xo[:na_out], yo[:na_out], s=45, c=color_na, marker=marker_na, label="Na⁺")
209
+ if k_out > 0:
210
+ ax.scatter(xo[na_out:], yo[na_out:], s=45, c=color_k, marker=marker_k, label="K⁺")
211
+ if na_in > 0:
212
+ ax.scatter(xi[:na_in], yi[:na_in], s=45, c=color_na, marker=marker_na)
213
+ if k_in > 0:
214
+ ax.scatter(xi[na_in:], yi[na_in:], s=45, c=color_k, marker=marker_k)
215
+
216
+ # Legend
217
+ legend_elements = [
218
+ Line2D([0], [0], marker=marker_na, color='w', label='Na⁺ (Blue • Circle)',
219
+ markerfacecolor=color_na, markersize=10),
220
+ Line2D([0], [0], marker=marker_k, color='w', label='K⁺ (Orange • Square)',
221
+ markerfacecolor=color_k, markersize=10),
222
+ ]
223
+ ax.legend(handles=legend_elements, loc='upper left', bbox_to_anchor=(0.01, 0.98), frameon=False)
224
+
225
+ # Stamp preset
226
+ ax.text(0.98, 0.05, f"{st.session_state.ap_preset}", transform=ax.transAxes,
227
+ ha="right", va="bottom", fontsize=11, alpha=0.8)
228
+
229
+ st.pyplot(fig, clear_figure=True)