ProfRick's picture
Update app.py
c53cae7 verified
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.")