import streamlit as st import numpy as np import matplotlib.pyplot as plt from matplotlib.patches import Circle, Arc, Rectangle from matplotlib.lines import Line2D st.set_page_config(page_title="Flow Down Gradients — Simple Explorer", layout="wide") # ---------------- Pressure presets (mmHg) ---------------- PRESSURE_PRESETS = { "Demo Preset": { "cap_o2": 52, "cap_co2": 49, "alv_o2": 55, "alv_co2": 45, "thoracic": 761, }, "Preset 1": { "cap_o2": 50, "cap_co2": 45, "alv_o2": 100, "alv_co2": 40, "thoracic": 763, }, "Preset 2": { "cap_o2": 60, "cap_co2": 45, "alv_o2": 58, "alv_co2": 40, "thoracic": 757, }, "Preset 3": { "cap_o2": 48, "cap_co2": 45, "alv_o2": 53, "alv_co2": 8, "thoracic": 768, }, } def get_pressure_preset_from_query(): qp = st.query_params key = qp.get("preset") if "preset" in qp else None if key in PRESSURE_PRESETS: return key return "Demo Preset" if "pressure_preset" not in st.session_state: st.session_state.pressure_preset = get_pressure_preset_from_query() # ---------------- Action Potential presets ---------------- AP_PRESETS = { "Preset 1": {"more_na": "Extracellular", "more_k": "Intracellular", "more_neg": "Intracellular"}, "Preset 2": {"more_na": "Intracellular", "more_k": "Extracellular", "more_neg": "Extracellular"}, "Preset 3": {"more_na": "Extracellular", "more_k": "Intracellular", "more_neg": "Extracellular"}, } if "ap_preset" not in st.session_state: st.session_state.ap_preset = "Preset 1" # ---------------- UI ---------------- st.title("Flow Down Gradients — Simple Explorer") tab1, tab2 = st.tabs([ "Pressure (Alveolus ↔ Capillary)", "Concentration: Action Potential (extracellular ↔ intracellular)", ]) # ---------------- Tab 1: Pressure ---------------- with tab1: st.subheader("Pressure Gradients (mmHg)") # Preset buttons with unique keys bcols = st.columns(4) for i, (name, col) in enumerate(zip(PRESSURE_PRESETS.keys(), bcols)): if col.button(name, key=f"pressure_btn_{i}_{name}", use_container_width=True): st.session_state.pressure_preset = name # Current preset preset_name = st.session_state.pressure_preset p = PRESSURE_PRESETS[preset_name] # Values st.markdown(f"**Current preset:** {preset_name}") st.markdown( f"- **Intracapillary O₂:** {p['cap_o2']} mmHg \n" f"- **Intracapillary CO₂:** {p['cap_co2']} mmHg \n" f"- **Intraalveolar O₂:** {p['alv_o2']} mmHg \n" f"- **Intraalveolar CO₂:** {p['alv_co2']} mmHg \n" f"- **Thoracic pressure:** {p['thoracic']} mmHg" ) # Diagram (enlarged, placed below values) fig, ax = plt.subplots(figsize=(11.5, 5.6)) ax.set_xlim(0, 12); ax.set_ylim(0, 6); ax.axis("off") # Alveolus: round sac alve_center = (4.0, 3.0); alve_r = 2.1 alve = Circle(alve_center, alve_r, fill=False, lw=2) ax.add_patch(alve) ax.text(alve_center[0], alve_center[1] + alve_r + 0.35, "Alveolus", ha="center", fontsize=13) # Capillary: thin vessel hugging right side (two arcs + short straight segments) cap_outer = Arc((6.2, 3.0), 3.6, 3.6, angle=0, theta1=-65, theta2=65, lw=6, capstyle="round") cap_inner = Arc((6.2, 3.0), 3.0, 3.0, angle=0, theta1=-65, theta2=65, lw=6, capstyle="round") ax.add_patch(cap_outer); ax.add_patch(cap_inner) # Straight ends to right ax.plot([7.8, 11.0], [4.2, 4.2], lw=6, solid_capstyle="round") ax.plot([7.8, 11.0], [1.8, 1.8], lw=6, solid_capstyle="round") ax.text(11.2, 3.0, "Capillary", va="center", fontsize=13, rotation=90) # Accessibility-friendly markers/colors color_o2, marker_o2 = "tab:blue", "o" # O2 = blue circle color_co2, marker_co2 = "tab:orange", "s" # CO2 = orange square # Dot counts scaled from mmHg (cap at 150 points per region) maxdots = 150 ao2 = int(np.clip(p["alv_o2"], 0, maxdots)) aco2 = int(np.clip(p["alv_co2"], 0, maxdots)) co2 = int(np.clip(p["cap_o2"], 0, maxdots)) cco2 = int(np.clip(p["cap_co2"], 0, maxdots)) # Alveolar dots: random inside circle rngA = np.random.default_rng(101) need = ao2 + aco2 if need > 0: xs, ys = [], [] while len(xs) < need: x = rngA.uniform(alve_center[0]-alve_r+0.12, alve_center[0]+alve_r-0.12) y = rngA.uniform(alve_center[1]-alve_r+0.12, alve_center[1]+alve_r-0.12) if (x-alve_center[0])**2 + (y-alve_center[1])**2 <= (alve_r-0.18)**2: xs.append(x); ys.append(y) xs = np.array(xs); ys = np.array(ys) if ao2 > 0: ax.scatter(xs[:ao2], ys[:ao2], s=36, c=color_o2, marker=marker_o2) if aco2 > 0: ax.scatter(xs[ao2:], ys[ao2:], s=36, c=color_co2, marker=marker_co2) # Capillary dots: right rectangular segment for clarity rngC = np.random.default_rng(202) cx = rngC.uniform(8.2, 10.8, co2 + cco2) cy = rngC.uniform(1.95, 4.05, co2 + cco2) if co2 > 0: ax.scatter(cx[:co2], cy[:co2], s=36, c=color_o2, marker=marker_o2) if cco2 > 0: ax.scatter(cx[co2:], cy[co2:], s=36, c=color_co2, marker=marker_co2) # Clean, accessible legend using Line2D handles (not cramped) legend_elements = [ Line2D([0], [0], marker=marker_o2, color='w', label='O₂ (Blue • Circle)', markerfacecolor=color_o2, markersize=10), Line2D([0], [0], marker=marker_co2, color='w', label='CO₂ (Orange • Square)', markerfacecolor=color_co2, markersize=10), ] ax.legend(handles=legend_elements, loc='upper left', bbox_to_anchor=(0.02, 0.98), frameon=False) # Stamp preset name ax.text(0.98, 0.05, f"{preset_name}", transform=ax.transAxes, ha="right", va="bottom", fontsize=11, alpha=0.8) st.pyplot(fig, clear_figure=True) # ---------------- Tab 2: Action Potential (presets, no sliders) ---------------- with tab2: st.subheader("Static starting conditions (no animation)") # Preset buttons with unique keys bcols = st.columns(3) for i, (name, col) in enumerate(zip(AP_PRESETS.keys(), bcols)): if col.button(name, key=f"ap_btn_{i}_{name}", use_container_width=True): st.session_state.ap_preset = name ap = AP_PRESETS[st.session_state.ap_preset] st.markdown(f"**Current preset:** {st.session_state.ap_preset}") st.markdown( f"- More **Na⁺**: {ap['more_na']} \n" f"- More **K⁺**: {ap['more_k']} \n" f"- More **negative**: {ap['more_neg']}" ) fig, ax = plt.subplots(figsize=(11.5, 5.0)) ax.set_xlim(0, 12); ax.set_ylim(0, 6); ax.axis("off") # Compartments ax.add_patch(Rectangle((0.8, 1.0), 4.5, 4.0, fill=False, lw=2)) # Extracellular ax.add_patch(Rectangle((6.7, 1.0), 4.5, 4.0, fill=False, lw=2)) # Intracellular ax.plot([6.2, 6.2], [1.0, 5.0], lw=8) # Membrane ax.text(3.05, 5.25, "Extracellular", ha="center", fontsize=12) ax.text(8.95, 5.25, "Intracellular", ha="center", fontsize=12) # Polarity signs ABOVE labels if ap["more_neg"] == "Intracellular": ax.text(3.05, 5.85, "+", ha="center", va="center", fontsize=24, alpha=0.6) ax.text(8.95, 5.85, "−", ha="center", va="center", fontsize=24, alpha=0.9) else: ax.text(3.05, 5.85, "−", ha="center", va="center", fontsize=24, alpha=0.9) ax.text(8.95, 5.85, "+", ha="center", va="center", fontsize=24, alpha=0.6) # Colors + shapes (accessibility): Na+ = blue circle, K+ = orange square color_na, marker_na = "tab:blue", "o" color_k, marker_k = "tab:orange", "s" # Dot counts: base + extra on "more" side base_amt = 12 extra_amt = 12 na_out = base_amt + (extra_amt if ap["more_na"] == "Extracellular" else 0) na_in = base_amt + (extra_amt if ap["more_na"] == "Intracellular" else 0) k_out = base_amt + (extra_amt if ap["more_k"] == "Extracellular" else 0) k_in = base_amt + (extra_amt if ap["more_k"] == "Intracellular" else 0) rng2 = np.random.default_rng(808) xo = rng2.uniform(1.1, 5.0, na_out + k_out); yo = rng2.uniform(1.2, 4.8, na_out + k_out) xi = rng2.uniform(7.0, 11.4, na_in + k_in ); yi = rng2.uniform(1.2, 4.8, na_in + k_in ) # Draw ions with fixed color+shape per type, regardless of side if na_out > 0: ax.scatter(xo[:na_out], yo[:na_out], s=45, c=color_na, marker=marker_na, label="Na⁺") if k_out > 0: ax.scatter(xo[na_out:], yo[na_out:], s=45, c=color_k, marker=marker_k, label="K⁺") if na_in > 0: ax.scatter(xi[:na_in], yi[:na_in], s=45, c=color_na, marker=marker_na) if k_in > 0: ax.scatter(xi[na_in:], yi[na_in:], s=45, c=color_k, marker=marker_k) # Legend legend_elements = [ Line2D([0], [0], marker=marker_na, color='w', label='Na⁺ (Blue • Circle)', markerfacecolor=color_na, markersize=10), Line2D([0], [0], marker=marker_k, color='w', label='K⁺ (Orange • Square)', markerfacecolor=color_k, markersize=10), ] ax.legend(handles=legend_elements, loc='upper left', bbox_to_anchor=(0.01, 0.98), frameon=False) # Stamp preset ax.text(0.98, 0.05, f"{st.session_state.ap_preset}", transform=ax.transAxes, ha="right", va="bottom", fontsize=11, alpha=0.8) st.pyplot(fig, clear_figure=True)