Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Third-party LaTeX template files under paper/ are kept verbatim.
exclude: ^paper/(sn-jnl\.cls|sn-nature\.bst|template-.*|main\.pdf)$
# Kept verbatim, never rewritten by hooks: third-party LaTeX template files
# under paper/, and scripts/gemini/out/ (GEMINI's own exported reports).
exclude: ^(paper/(sn-jnl\.cls|sn-nature\.bst|template-.*|main\.pdf)|scripts/gemini/out/.*)$
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0 # Use the ref you want to point at
Expand Down Expand Up @@ -42,7 +43,10 @@ repos:
entry: python3 -m mypy --config-file pyproject.toml
language: system
types: [python]
exclude: "tests"
# tests, plus the cohort producers committed verbatim as they ran
# (scripts/cohort/README.md); pyproject's mypy exclude does not apply
# to files pre-commit passes by name.
exclude: ^(tests/|scripts/cohort/cohort_check_(mimic|eicu)\.py$)

- repo: https://github.com/crate-ci/typos
rev: v1
Expand All @@ -56,7 +60,7 @@ repos:
# etc.), not ours to rename; see _typos.toml for the same exclusion
# (this one is what actually stops the hook, since pre-commit's own
# file filtering runs before typos ever sees its own config)
exclude: ^(docs/experiments\.md|odyssey/data/resources/.*\.csv|scripts/gemini/out/.*)$
exclude: ^(docs/experiments\.md|odyssey/data/resources/.*\.csv|scripts/gemini/out/.*|scripts/cohort/cohort_check_(mimic|eicu)\.py)$

- repo: https://github.com/nbQA-dev/nbQA
rev: 1.9.1
Expand Down
8 changes: 7 additions & 1 deletion _typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,10 @@ get_rolling_window_indicies = "get_rolling_window_indicies"
# ours to rename and will keep showing up as new false positives every time
# a fresh report is committed, so the whole directory is excluded rather
# than growing an extend-words entry per column.
extend-exclude = ["scripts/gemini/out/"]
# scripts/cohort/cohort_check_*.py are committed verbatim as they ran
# (scripts/cohort/README.md); their `evn` (eICU visit number) stays.
extend-exclude = [
"scripts/gemini/out/",
"scripts/cohort/cohort_check_mimic.py",
"scripts/cohort/cohort_check_eicu.py",
]
16 changes: 16 additions & 0 deletions apps/clinician_demo/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Clinician demo: replay a patient's chart and watch the model's risk forecasts.

A small web app that runs a trained Odyssey checkpoint on one patient at a
time and shows, in plain clinical language, how its risks (vasopressors,
ICU admission, AKI, Sepsis-3, death) evolve through an admission, when
they would have raised an alert, what the model thinks is going on, what
would move the forecast, and how good the model is. Runs on the GPU host,
bound to loopback, viewed through an SSH tunnel: patient-level data never
leaves the host. See ``docs/clinician_demo.md``.

Layers, torch only below the service: ``schemas`` (JSON contracts) ->
``codebook`` / ``patient_store`` / ``thresholds`` / ``showcase`` /
``scorecard`` (pure data) -> ``forecast`` / ``whatif`` / ``evidence``
(model) -> ``service`` (orchestration, GPU lock, caches) -> ``server``
(HTTP) -> ``static/`` (rendering only).
"""
122 changes: 122 additions & 0 deletions apps/clinician_demo/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""Start the clinician demo (or run its self-check).

Usage, on the GPU host from the repository root::

.venv/bin/python -m apps.clinician_demo \\
--run-dir ~/runs/full_run_v10 \\
--data-dir ~/data/mimiciv_3.1_v1/data/held_out \\
--metadata-dir ~/data/mimiciv_3.1_v1/metadata \\
--splits ~/data/mimiciv_3.1_v1/metadata/subject_splits.parquet

then, on the laptop, open a tunnel and browse to http://localhost:8765::

gcloud compute ssh <vm> --zone <zone> --project <project> \\
--tunnel-through-iap -- -N -L 8765:localhost:8765

``--data-mode open --data-dir ~/data/mimiciv_demo_meds/data`` serves the
open MIMIC-IV demo instead. ``--self-check`` loads everything, measures one
case end to end, prints a JSON report and exits non-zero on failure.
"""

import argparse
import json
import logging
import sys
from pathlib import Path

from apps.clinician_demo.config import DATA_MODES, DemoConfig


logger = logging.getLogger(__name__)


def parse_args(argv: list[str] | None = None) -> tuple[DemoConfig, bool]:
"""Parse the command line into a config and the self-check flag."""
parser = argparse.ArgumentParser(
prog="python -m apps.clinician_demo",
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("--run-dir", type=Path, required=True)
parser.add_argument(
"--data-dir", type=Path, required=True, help="MEDS shard directory"
)
parser.add_argument(
"--metadata-dir", type=Path, default=None, help="MEDS metadata/ (codes.parquet)"
)
parser.add_argument(
"--splits", type=Path, default=None, help="the model's subject_splits.parquet"
)
parser.add_argument("--data-mode", choices=DATA_MODES, default="credentialed")
parser.add_argument("--checkpoint", default="checkpoint_best.pt")
parser.add_argument("--port", type=int, default=8765)
parser.add_argument("--alert-rate", type=float, default=0.05)
parser.add_argument("--max-shards", type=int, default=None)
parser.add_argument("--cache-dir", type=Path, default=None)
parser.add_argument("--device", default="cuda")
parser.add_argument("--no-warmup", action="store_true")
parser.add_argument("--self-check", action="store_true")
args = parser.parse_args(argv)
try:
config = DemoConfig(
run_dir=args.run_dir.expanduser(),
data_dir=args.data_dir.expanduser(),
metadata_dir=args.metadata_dir.expanduser() if args.metadata_dir else None,
splits_path=args.splits.expanduser() if args.splits else None,
data_mode=args.data_mode,
checkpoint=args.checkpoint,
port=args.port,
alert_rate=args.alert_rate,
max_shards=args.max_shards,
cache_dir=args.cache_dir.expanduser() if args.cache_dir else None,
device=args.device,
warmup=not args.no_warmup,
)
except ValueError as exc:
parser.error(str(exc))
return config, args.self_check


def main(argv: list[str] | None = None) -> int:
"""Run the demo server, or the self-check; return the process exit code."""
logging.basicConfig(
level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s"
)
config, self_check = parse_args(argv)
# Deferred: loading torch and the model is the slow part, and argument
# errors should surface before it.
from apps.clinician_demo.server import make_server # noqa: PLC0415
from apps.clinician_demo.service import DemoService # noqa: PLC0415

service = DemoService.from_config(config)
if self_check:
try:
print(json.dumps(service.self_check(), indent=2, default=str))
except Exception:
logger.exception("[self-check] FAILED")
return 1
finally:
service.shutdown()
return 0
if config.warmup:
service.warm_up()
server = make_server(service, config.host, config.port)
logger.info(
"serving %s (%s mode) on http://%s:%d -- open an SSH tunnel to this port",
config.run_name,
config.data_mode,
config.host,
config.port,
)
try:
server.serve_forever()
except KeyboardInterrupt:
logger.info("stopping")
finally:
server.server_close()
service.shutdown()
return 0


if __name__ == "__main__":
sys.exit(main())
Loading