CoolFace
Apppublic

marvelous1/prostate-mri-2afc

sourceHugging Facemitupdated 5mo agoView on Hugging Face
0likes
database.py231 linesDownload Raw Back to root
1"""SQLAlchemy models for Prostate MRI Reader Study Platform.2 3Schema covers: readers, cases, reading sessions (3-arm MRMC),4scores, turing test items + responses, audit log.5"""6 7import os8from datetime import datetime9from sqlalchemy import (10    Column, Integer, String, Float, Boolean, DateTime, ForeignKey, Text,11    UniqueConstraint, Index, create_engine,12)13from sqlalchemy.orm import declarative_base, relationship, sessionmaker14 15BASE_DIR = os.path.dirname(os.path.abspath(__file__))16DB_PATH = os.path.join(BASE_DIR, "db", "readerstudy.db")17os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)18 19DB_URL = f"sqlite:///{DB_PATH}"20engine = create_engine(DB_URL, connect_args={"check_same_thread": False})21SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)22Base = declarative_base()23 24 25class Reader(Base):26    __tablename__ = "readers"27    id = Column(Integer, primary_key=True)28    username = Column(String(64), unique=True, nullable=False, index=True)29    password_hash = Column(String(256), nullable=False)30    display_name = Column(String(128))31    center = Column(String(128))               # e.g. "Renji Hospital"32    level = Column(String(32))                 # "senior" | "mid" | "junior"33    experience_years = Column(Integer)34    is_admin = Column(Boolean, default=False)35    arm_order = Column(String(16), default="1,2,3")  # randomized order per-reader36    created_at = Column(DateTime, default=datetime.utcnow)37 38    sessions = relationship("ReadingSession", back_populates="reader")39    turing_responses = relationship("TuringResponse", back_populates="reader")40 41 42class Case(Base):43    __tablename__ = "cases"44    id = Column(Integer, primary_key=True)45    case_code = Column(String(32), unique=True, nullable=False, index=True)  # R2024-000146    task_type = Column(String(8))              # "A" | "B" | "C" | "D"47    # JSON encoded list of sequences available in each arm48    # Arm 1: bpMRI (no target), Arm 2: bpMRI + AI target, Arm 3: full (real target)49    arm1_sequences = Column(String(256))       # e.g. "T2WI,DWI,ADC"50    arm2_sequences = Column(String(256))       # e.g. "T2WI,DWI,ADC,T1_AI"51    arm3_sequences = Column(String(256))       # e.g. "T2WI,DWI,ADC,T1_real"52    num_slices = Column(Integer, default=32)53    # Reference standard (hidden from readers, used for analysis)54    gt_pirads = Column(Integer)                # expert consensus PI-RADS55    gt_csPCa = Column(Boolean)                 # ISUP >= 2 by pathology56    gt_lesion_location = Column(String(64))    # e.g. "PZ-left-mid"57    prostate_volume_ml = Column(Float)58    notes = Column(Text)59    created_at = Column(DateTime, default=datetime.utcnow)60 61    sessions = relationship("ReadingSession", back_populates="case")62 63 64class ReadingSession(Base):65    """One entry = one (reader, case, arm) tuple."""66    __tablename__ = "reading_sessions"67    id = Column(Integer, primary_key=True)68    reader_id = Column(Integer, ForeignKey("readers.id"), nullable=False)69    case_id = Column(Integer, ForeignKey("cases.id"), nullable=False)70    arm = Column(Integer, nullable=False)      # 1, 2, or 371    order_in_arm = Column(Integer)             # randomized position in reader's queue72    status = Column(String(16), default="pending")  # pending/started/completed/skipped73    started_at = Column(DateTime)74    completed_at = Column(DateTime)75    time_spent_sec = Column(Integer, default=0)76 77    reader = relationship("Reader", back_populates="sessions")78    case = relationship("Case", back_populates="sessions")79    score = relationship("Score", uselist=False, back_populates="session")80 81    __table_args__ = (82        UniqueConstraint("reader_id", "case_id", "arm", name="uq_reader_case_arm"),83        Index("idx_reader_status", "reader_id", "status"),84    )85 86 87class Score(Base):88    __tablename__ = "scores"89    id = Column(Integer, primary_key=True)90    session_id = Column(Integer, ForeignKey("reading_sessions.id"), unique=True, nullable=False)91 92    # PI-RADS primary scoring93    pi_rads = Column(Integer)                  # 1-594    lesion_location = Column(String(64))       # sector95    lesion_size_mm = Column(Float)96    epe_status = Column(String(16))            # "yes"/"no"/"equivocal"97    svi_status = Column(String(16))            # "yes"/"no"98 99    # Image quality Likert per sequence (1-5)100    t2_quality = Column(Integer)101    dwi_quality = Column(Integer)102    adc_quality = Column(Integer)103    t1_quality = Column(Integer)104 105    # Per-case judgments106    diagnostic_confidence = Column(Integer)    # 1-5107    acceptable_for_clinical = Column(String(32))  # "yes"/"no"/"reservations"108 109    # Optional Turing-embedded judgment (Arm 2 only)110    turing_is_ai = Column(String(16))          # "real"/"ai"/"unsure"111    turing_confidence = Column(Integer)112 113    comments = Column(Text)114    submitted_at = Column(DateTime, default=datetime.utcnow)115 116    session = relationship("ReadingSession", back_populates="score")117 118 119class TuringItem(Base):120    __tablename__ = "turing_items"121    id = Column(Integer, primary_key=True)122    image_path = Column(String(512))           # path to rendered image123    sequence_type = Column(String(16))         # "T1WI"/"DWI-highb" etc.124    case_code = Column(String(32))125    slice_idx = Column(Integer)126    is_ai = Column(Boolean, nullable=False)    # True = AI-generated, False = real127    created_at = Column(DateTime, default=datetime.utcnow)128 129 130class TuringResponse(Base):131    __tablename__ = "turing_responses"132    id = Column(Integer, primary_key=True)133    reader_id = Column(Integer, ForeignKey("readers.id"), nullable=False)134    item_id = Column(Integer, ForeignKey("turing_items.id"), nullable=False)135    judgment = Column(String(16))              # "real"/"ai"/"unsure"136    confidence = Column(Integer)137    time_spent_sec = Column(Integer)138    submitted_at = Column(DateTime, default=datetime.utcnow)139 140    reader = relationship("Reader", back_populates="turing_responses")141 142    __table_args__ = (143        UniqueConstraint("reader_id", "item_id", name="uq_reader_turing_item"),144    )145 146 147class BlindedCase(Base):148    """A single 2AFC Turing-test card. One of A/B is AI-enhanced GT, the149    other is real GT. Ground-truth mapping (ai_position) is hidden from150    readers and only used for post-hoc analysis."""151    __tablename__ = "blinded_cases"152    id = Column(Integer, primary_key=True)153    case_code = Column(String(16), unique=True, nullable=False, index=True)   # R-0001154    png_path = Column(String(512), nullable=False)                            # relative to static/ or abs155    patient_id_full = Column(String(256))                                     # Renji DICOM uid (never shown)156    task_type = Column(String(4))                                             # A-G157    target_seq = Column(String(8))                                            # T2WI/DWI/ADC/T1WI158    input_seqs = Column(String(64))                                           # "T2WI+ADC+T1WI"159    ai_position = Column(String(4))                                           # "A" or "B" (hidden from readers)160    ssim = Column(Float)161    psnr = Column(Float)162    ncc = Column(Float)163    blend_alpha = Column(Float)164    created_at = Column(DateTime, default=datetime.utcnow)165 166 167class BlindedAssignment(Base):168    """Which reader reads which case, in what order."""169    __tablename__ = "blinded_assignments"170    id = Column(Integer, primary_key=True)171    reader_id = Column(Integer, ForeignKey("readers.id"), nullable=False, index=True)172    blinded_case_id = Column(Integer, ForeignKey("blinded_cases.id"), nullable=False, index=True)173    order_in_queue = Column(Integer, nullable=False)174    status = Column(String(16), default="pending")            # pending | started | completed | skipped175    started_at = Column(DateTime)176    completed_at = Column(DateTime)177    time_spent_sec = Column(Integer, default=0)178 179    __table_args__ = (180        UniqueConstraint("reader_id", "blinded_case_id", name="uq_reader_blinded_case"),181        Index("idx_reader_blinded_status", "reader_id", "status"),182    )183 184 185class BlindedResponse(Base):186    """Reader's answer on a 2AFC card (5 items)."""187    __tablename__ = "blinded_responses"188    id = Column(Integer, primary_key=True)189    assignment_id = Column(Integer, ForeignKey("blinded_assignments.id"), unique=True, nullable=False)190    reader_id = Column(Integer, ForeignKey("readers.id"), nullable=False, index=True)191    blinded_case_id = Column(Integer, ForeignKey("blinded_cases.id"), nullable=False, index=True)192 193    q1_ai_choice = Column(String(16))                          # "A" / "B" / "Cannot tell"194    q2_quality_a = Column(Integer)                             # 1-5195    q2_quality_b = Column(Integer)196    q3_anatomy_a = Column(Integer)197    q3_anatomy_b = Column(Integer)198    q4_preference = Column(String(16))                         # "A" / "B" / "Equivalent"199    q5_confidence = Column(Integer)                            # 1-5 (Guessing..Certain)200    comments = Column(Text)201    time_spent_sec = Column(Integer)202    submitted_at = Column(DateTime, default=datetime.utcnow)203 204 205class AuditLog(Base):206    __tablename__ = "audit_log"207    id = Column(Integer, primary_key=True)208    reader_id = Column(Integer, ForeignKey("readers.id"))209    event = Column(String(64))                 # "login"/"view_case"/"submit_score"210    detail = Column(Text)211    ip_address = Column(String(64))212    user_agent = Column(String(256))213    timestamp = Column(DateTime, default=datetime.utcnow)214 215 216def init_db():217    Base.metadata.create_all(bind=engine)218 219 220def get_db():221    db = SessionLocal()222    try:223        yield db224    finally:225        db.close()226 227 228if __name__ == "__main__":229    init_db()230    print(f"Database initialized at {DB_PATH}")231