Spaces:
Runtime error
Runtime error
| import streamlit as st | |
| import random | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| from typing import List, Dict, Tuple, Set | |
| import io, datetime | |
| st.set_page_config(page_title="Pharmacodynamics Review", page_icon="π", layout="wide") | |
| # ============================================== | |
| # Session state | |
| # ============================================== | |
| if "unlock" not in st.session_state: | |
| st.session_state.unlock = {"effects": False, "combos": False} | |
| if "student_name" not in st.session_state: | |
| st.session_state.student_name = "" | |
| if "combo_answers" not in st.session_state: | |
| st.session_state.combo_answers = {} | |
| # ============================================== | |
| # DATA β DEA schedules (federal) + Non-scheduled | |
| # ============================================== | |
| DEA_SCHEDULES: Dict[str, str] = { | |
| # Schedule I | |
| "Heroin": "I", | |
| "LSD (acid)": "I", | |
| "MDMA (Ecstasy/Molly)": "I", | |
| "Psilocybin (mushrooms)": "I", | |
| "Cannabis/Marijuana (federal status)": "I", | |
| # Schedule II | |
| "Cocaine": "II", | |
| "Methamphetamine": "II", | |
| "Fentanyl": "II", | |
| "Oxycodone": "II", | |
| "Amphetamine (Adderall)": "II", | |
| # Schedule III | |
| "Ketamine": "III", | |
| "Anabolic steroids": "III", | |
| # Schedule IV | |
| "Alprazolam (Xanax)": "IV", | |
| "Diazepam (Valium)": "IV", | |
| # Non-scheduled (included for comparison in this exercise) | |
| "Alcohol (ethanol)": "Non-scheduled", | |
| "Nicotine/Tobacco": "Non-scheduled", | |
| "Kratom": "Non-scheduled", | |
| "Cannabidiol (CBD)": "Non-scheduled", | |
| } | |
| SCHEDULE_ORDER = ["I", "II", "III", "IV", "V", "Non-scheduled"] | |
| # Randomize drug order once per session | |
| if "drug_order" not in st.session_state: | |
| st.session_state.drug_order = random.sample(list(DEA_SCHEDULES.keys()), k=len(DEA_SCHEDULES)) | |
| # ============================================== | |
| # Acute effects β categories (updated naming) | |
| # ============================================== | |
| NIDA_CATEGORIES: List[str] = [ | |
| "Alcohol", | |
| "Cannabinoids", | |
| "Opioids", | |
| "Stimulants", | |
| "Club Drugs", # was Depressants (benzos, GHB) | |
| "Dissociative Drugs", | |
| "Hallucinogens", | |
| "Tobacco/Nicotine", | |
| ] | |
| # Rules per effect: | |
| # - required: must be selected | |
| # - neutral: selection does not matter (either included or not) | |
| # Validation: valid iff (required β chosen) and (chosen β© forbidden == β ) | |
| ALL_CATS: Set[str] = set(NIDA_CATEGORIES) | |
| EFFECT_RULES: Dict[str, Dict[str, Set[str]]] = { | |
| # 1) Euphoria β Alcohol, Cannabinoids, Opioids required; Dissociative Drugs neutral | |
| "Euphoria": { | |
| "required": {"Alcohol", "Cannabinoids", "Opioids"}, | |
| "neutral": {"Dissociative Drugs"}, | |
| }, | |
| # 2) Loss of/Impaired Coordination β Alcohol, Cannabinoids, Opioids required; Club & Dissociative neutral | |
| "Loss of/Impaired Coordination": { | |
| "required": {"Alcohol", "Cannabinoids", "Opioids"}, | |
| "neutral": {"Club Drugs", "Dissociative Drugs"}, | |
| }, | |
| # 3) Anxiety/panic β Stimulants & Cannabinoids required; Hallucinogens neutral | |
| "Anxiety/panic": { | |
| "required": {"Stimulants", "Cannabinoids"}, | |
| "neutral": {"Hallucinogens"}, | |
| }, | |
| } | |
| # ============================================== | |
| # Combination gallery β use only additive/synergistic/antagonistic examples | |
| # (Removed the mixed example to allow clear grading/completion.) | |
| # ============================================== | |
| COMBOS = [ | |
| {"name": "Alcohol + Benzodiazepine", "class": "Synergistic"}, | |
| {"name": "Opioid (e.g., heroin) + Benzodiazepine", "class": "Synergistic"}, | |
| {"name": "Alcohol + Cannabis", "class": "Additive"}, | |
| {"name": "Naloxone + Opioid", "class": "Antagonistic"}, | |
| ] | |
| # ============================================== | |
| # Helpers | |
| # ============================================== | |
| def sigmoid(x, mid=30, steep=0.12, top=1.0): | |
| return top / (1 + np.exp(-steep * (x - mid))) | |
| def header() -> None: | |
| st.title("π Pharmacodynamics Review (Foundations)") | |
| st.markdown( | |
| """ | |
| Goals today | |
| 1) DEA scheduling basics (plus *non-scheduled* examples) | |
| 2) Recognize acute effects and legitimate overlap across categories | |
| 3) Identify drug-combo interactions by inspecting a combined effect graph | |
| Designed for asynchronous learning; illustrative only. | |
| """ | |
| ) | |
| name = st.text_input("Insert First and Last Name *", value=st.session_state.get("student_name", "")).strip() | |
| st.session_state.student_name = name | |
| if not name: | |
| st.info("Please enter your first and last name before proceeding to the next section.") | |
| # ============================================== | |
| # 1) DEA Schedule Sorting (randomized, strict gating) | |
| # ============================================== | |
| def schedule_sorter(): | |
| st.subheader("1) DEA Schedule Sorting Challenge") | |
| st.caption("Assign each drug to a schedule. All items must be correct to unlock the next section.") | |
| assignments: Dict[str, str] = {} | |
| all_answered = True | |
| for d in st.session_state.drug_order: | |
| st.markdown(f"**{d}**") | |
| sel = st.selectbox( | |
| "Select schedule", | |
| options=["β Select β"] + SCHEDULE_ORDER, | |
| index=0, | |
| key=f"sched_{d}" | |
| ) | |
| assignments[d] = sel | |
| if sel == "β Select β": | |
| all_answered = False | |
| st.divider() | |
| if st.button("Check my answers", type="primary"): | |
| if not st.session_state.student_name: | |
| st.warning("Please enter your **first and last name** above before checking.") | |
| return | |
| if not all_answered: | |
| st.warning("Please assign **every drug** before checking.") | |
| return | |
| correct = sum(1 for d in DEA_SCHEDULES if assignments.get(d) == DEA_SCHEDULES[d]) | |
| total = len(DEA_SCHEDULES) | |
| if correct == total: | |
| st.success("All correct! You've unlocked Acute Effects.") | |
| st.session_state.unlock["effects"] = True | |
| else: | |
| st.error(f"{total - correct} incorrect. Review and try again.") | |
| # ============================================== | |
| # 2) Acute Effects (updated list & rules; strict gating) | |
| # ============================================== | |
| def is_effect_selection_correct(effect: str, chosen: Set[str]) -> bool: | |
| rules = EFFECT_RULES[effect] | |
| required = rules["required"] | |
| neutral = rules["neutral"] | |
| forbidden = ALL_CATS - required - neutral | |
| return required.issubset(chosen) and chosen.isdisjoint(forbidden) | |
| def effects_overlap_quiz(): | |
| st.subheader("2) Acute Effects β Overlap Recognition") | |
| st.caption("Select all categories that apply for each effect. You must get every effect fully correct to proceed.") | |
| user_choices: Dict[str, List[str]] = {} | |
| for effect in EFFECT_RULES.keys(): | |
| st.markdown(f"**{effect}**") | |
| user_choices[effect] = st.multiselect( | |
| "Select categories:", options=NIDA_CATEGORIES, key=f"eff_{effect}") | |
| st.divider() | |
| if st.button("Check effects", type="primary"): | |
| wrong_effects = [] | |
| for effect in EFFECT_RULES.keys(): | |
| chosen = set(user_choices.get(effect, [])) | |
| if not is_effect_selection_correct(effect, chosen): | |
| wrong_effects.append(effect) | |
| if not wrong_effects: | |
| st.success("All effects answered correctly! You've unlocked Drug Combination Effects.") | |
| st.session_state.unlock["combos"] = True | |
| else: | |
| st.error("Some effects need review. The following are not correct:") | |
| for eff in wrong_effects: | |
| st.write(f"- {eff}") | |
| # ============================================== | |
| # 3) Drug Combination Effects β Bar graphs (fixed dose, improved synergy) | |
| # ============================================== | |
| def combo_base_values(choice: str) -> Tuple[float, float]: | |
| """Return (A_val, B_val) at a fixed mid-range dose chosen to avoid saturation.""" | |
| x = np.linspace(0, 100, 300) | |
| dose_fixed = 55 # remove slider per request; mid-range to avoid saturation | |
| # Use lower tops to keep sums below 1.0 so synergy is visible without clipping | |
| seed_map = {"Alcohol + Benzodiazepine": 1, | |
| "Opioid (e.g., heroin) + Benzodiazepine": 2, | |
| "Alcohol + Cannabis": 3, | |
| "Naloxone + Opioid": 4} | |
| rng = np.random.RandomState(seed_map.get(choice, 0)) | |
| midA = 25 + rng.randint(-4, 5) | |
| midB = 30 + rng.randint(-4, 5) | |
| steepA = 0.14 + rng.rand() * 0.04 | |
| steepB = 0.12 + rng.rand() * 0.04 | |
| # Tops < 1 to avoid immediate ceiling | |
| topA = 0.72 | |
| topB = 0.72 | |
| A = sigmoid(x, midA, steepA, topA) | |
| B = sigmoid(x, midB, steepB, topB) | |
| # sample at dose_fixed | |
| idx = np.searchsorted(x, dose_fixed) | |
| def interp(series): | |
| if idx <= 0: | |
| return float(series[0]) | |
| if idx >= len(series): | |
| return float(series[-1]) | |
| x0, x1 = x[idx-1], x[idx] | |
| y0, y1 = series[idx-1], series[idx] | |
| t = (dose_fixed - x0) / (x1 - x0) | |
| return float(y0 + t * (y1 - y0)) | |
| return interp(A), interp(B) | |
| def combo_gallery(): | |
| st.subheader("3) Drug Combination Effects β What interaction is this?") | |
| st.caption("We show Drug A, Drug B, and a combined effect as **bar graphs** at a fixed dose. Classify: additive, synergistic, or antagonistic.") | |
| # Fixed values provided by instructor (normalized 0β1) | |
| # Note: Interpreted 9.3 as 0.93 to keep within normalized display. | |
| FIXED_VALUES = { | |
| "Alcohol + Benzodiazepine": {"A": 0.20, "B": 0.20, "Combined": 0.90, "label": "Synergistic"}, | |
| "Opioid (e.g., heroin) + Benzodiazepine": {"A": 0.10, "B": 0.15, "Combined": 0.93, "label": "Synergistic"}, | |
| "Alcohol + Cannabis": {"A": 0.30, "B": 0.30, "Combined": 0.60, "label": "Additive"}, | |
| "Naloxone + Opioid": {"A": 0.20, "B": 0.60, "Combined": 0.15, "label": "Antagonistic"}, | |
| } | |
| choice = st.selectbox("Pick a combination:", list(FIXED_VALUES.keys()), key="combo_choice") | |
| # Labels for bars | |
| if " + " in choice: | |
| a_label, b_label = choice.split(" + ", 1) | |
| else: | |
| a_label, b_label = "Drug A", "Drug B" | |
| vals = FIXED_VALUES[choice] | |
| a_val, b_val, combo_val = vals["A"], vals["B"], vals["Combined"] | |
| # Plot bars | |
| fig, ax = plt.subplots(figsize=(6, 4)) | |
| bars = [a_label, b_label, "Combined"] | |
| ax.bar(bars, [a_val, b_val, combo_val]) | |
| ax.set_ylim(0, 1.0) | |
| ax.set_ylabel("Effect (normalized)") | |
| ax.set_title(f"{choice} @ fixed dose") | |
| st.pyplot(fig) | |
| guess = st.radio("This combined effect isβ¦", ["Additive", "Synergistic", "Antagonistic"], index=None, key=f"combo_guess_{choice}") | |
| if st.button("Check combo", type="primary"): | |
| if guess is None: | |
| st.warning("Choose one option above.") | |
| else: | |
| # record correctness without revealing the answer | |
| is_correct = (guess == vals["label"]) | |
| st.session_state.combo_answers[choice] = is_correct | |
| if is_correct: | |
| st.success("Correct") | |
| else: | |
| st.error("Incorrect selection") | |
| # If all combos have been answered correctly, offer PDF submission | |
| all_correct = all(st.session_state.combo_answers.get(c["name"], False) for c in COMBOS) | |
| if all_correct and st.session_state.student_name: | |
| st.divider() | |
| st.markdown("### Submit") | |
| st.caption("Download and upload this completion PDF to Canvas.") | |
| def build_pdf_bytes(name: str) -> bytes: | |
| """Create a completion PDF. Falls back to a text file if reportlab is unavailable.""" | |
| try: | |
| from reportlab.lib.pagesizes import letter | |
| from reportlab.pdfgen import canvas | |
| except Exception: | |
| # Fallback: plain text bytes (safe single-line joins) | |
| timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| lines = [ | |
| "Pharmacodynamics Review - Completion Certificate", | |
| f"Student: {name}", | |
| f"Date: {timestamp}", | |
| "Sections completed: DEA Schedules, Acute Effects, Drug Combos", | |
| ] | |
| txt = "\n".join(lines) + "\n" | |
| return txt.encode("utf-8") | |
| buffer = io.BytesIO() | |
| c = canvas.Canvas(buffer, pagesize=letter) | |
| w, h = letter | |
| c.setFont("Helvetica-Bold", 18) | |
| c.drawString(72, h - 72, "Pharmacodynamics Review - Completion Certificate") | |
| c.setFont("Helvetica", 12) | |
| c.drawString(72, h - 110, f"Student: {name}") | |
| c.drawString(72, h - 130, f"Date: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") | |
| c.drawString(72, h - 150, "Sections completed: DEA Schedules, Acute Effects, Drug Combos") | |
| c.showPage() | |
| c.save() | |
| buffer.seek(0) | |
| return buffer.getvalue() | |
| pdf_bytes = build_pdf_bytes(st.session_state.student_name) | |
| st.download_button( | |
| label="Submit (Download PDF)", | |
| data=pdf_bytes, | |
| file_name="pharmacodynamics_completion.pdf", | |
| mime="application/pdf", | |
| type="primary", | |
| ) | |
| # ============================================== | |
| # Sidebar progress | |
| # ============================================== | |
| def sidebar_progress(): | |
| with st.sidebar: | |
| st.header("Module Progress") | |
| st.write(f"Name provided: {'β ' if st.session_state.student_name else 'β'}") | |
| st.write(f"Acute Effects unlocked: {'β ' if st.session_state.unlock['effects'] else 'β'}") | |
| # Count correctly answered combos | |
| correct_count = sum(1 for c in COMBOS if st.session_state.combo_answers.get(c['name'], False)) | |
| st.write(f"Combos correct: {correct_count}/{len(COMBOS)}") | |
| st.caption("Educational demo. Federal scheduling shown; state laws vary.") | |
| # ============================================== | |
| # Layout (gated tabs) | |
| # ============================================== | |
| header() | |
| sidebar_progress() | |
| TAB1, TAB2, TAB3 = st.tabs(["DEA Schedules", "Acute Effects", "Drug Combos"]) | |
| with TAB1: | |
| schedule_sorter() | |
| with TAB2: | |
| if st.session_state.unlock["effects"]: | |
| effects_overlap_quiz() | |
| else: | |
| if st.session_state.student_name: | |
| st.info("π Complete the DEA Schedule task correctly to unlock this section.") | |
| else: | |
| st.info("π Enter your name and complete the DEA Schedule task to unlock this section.") | |
| with TAB3: | |
| if st.session_state.unlock["combos"]: | |
| combo_gallery() | |
| else: | |
| st.info("π Complete the Acute Effects section correctly to unlock this section.") | |
| st.divider() | |
| st.caption("Β© 2025 β Teaching tool for foundational pharmacodynamics. For education only.") | |