-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathpyproject.toml
More file actions
361 lines (335 loc) · 14.9 KB
/
Copy pathpyproject.toml
File metadata and controls
361 lines (335 loc) · 14.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
[project]
name = "odyssey"
version = "0.1.0"
description = "A toolkit to develop clinical foundation models using electronic health records"
readme = "README.md"
authors = [{name = "Vector AI Engineering", email = "odyssey@vectorinstitute.ai"}]
license = "Apache-2.0"
repository = "https://github.com/VectorInstitute/odyssey"
documentation = "https://vectorinstitute.github.io/odyssey/"
requires-python = ">=3.12"
dependencies = [
# MIMIC-IV -> MEDS extraction (hosp + icu modules; ED not yet covered upstream)
"MIMIC_IV_MEDS",
# Used directly (not just transitively via MIMIC_IV_MEDS) for concept labeling.
"polars>=1.44.2",
# Model dev. Mac (local, CPU/MPS) installs plain torch, floating freely.
# On the GCP A100 host, torch is pinned to an exact cu124 build and
# mamba-ssm (below) to an exact version -- discovered the hard way that
# letting either float is a real trap here: torch>=2.12.1 alone resolved
# to a cu130 build on that host, but the host's system CUDA toolkit is
# 12.9 (no nvcc for 13.x), so mamba-ssm's CUDA extension couldn't be
# rebuilt against it and torch.utils.cpp_extension hard-fails on any
# major-version CUDA mismatch. torch==2.6.0+cu124 is the newest build on
# PyTorch's cu124 wheel index (its release series stops there), which
# matches CUDA 12.9's nvcc fine (only failed on exact-major mismatches,
# not minor). See the `cuda` extra below for the matching mamba-ssm pin.
"torch>=2.12.1; sys_platform != 'linux'",
"torch==2.6.0; sys_platform == 'linux'",
# urllib3>=2.7.0 fixes CVE-2026-44431 and CVE-2026-44432
"urllib3>=2.8.0",
# idna>=3.15 fixes CVE-2026-45409
"idna>=3.20",
# Eval metrics only (AUROC/AUPRC/Brier score for concept quality) --
# not used anywhere in the training/inference path itself.
"scikit-learn>=1.9.1",
]
[project.optional-dependencies]
# mamba-ssm requires CUDA/nvcc — GCP A100 only, not buildable on Mac.
# Pinned exact (not >=): 2.3.2 added a Mamba3 module that's imported
# eagerly by mamba_ssm/__init__.py (so even our own submodule-only
# imports trigger it) and needs a newer Triton than the one torch==2.6.0
# bundles. 2.3.0 doesn't have that module.
#
# Two separate steps -- do NOT collapse into one `uv sync --no-build-
# isolation` call, confirmed both ways fail:
# 1. `uv sync --extra cuda` (plain, isolated builds). This correctly
# installs the pinned torch and the rest of the project; mamba-ssm
# itself comes out broken from this step alone (see #2) but that's
# fine, it gets overwritten next. Passing --no-build-isolation
# *here* instead breaks the *local* `odyssey` package's own build
# (hatchling/editables aren't in the venv to satisfy it non-
# isolated) and doesn't even fix mamba-ssm, since mamba-ssm's own
# declared build requirements pull a fresh, unpinned (and thus
# ABI-mismatched) torch into its *own* isolated build env,
# regardless of this project's torch pin.
# 2. Force-rebuild mamba-ssm specifically, non-isolated so it reuses
# the already-installed, correctly-pinned torch instead of
# resolving its own:
# PATH=/usr/local/cuda-12.9/bin:$PATH \
# CUDA_HOME=/usr/local/cuda-12.9 MAX_JOBS=12 \
# MAMBA_FORCE_BUILD=TRUE \
# uv pip install --no-build-isolation --no-binary mamba-ssm \
# --no-deps --no-cache --reinstall 'mamba-ssm==2.3.0'
# (--no-binary forces a real source build -- mamba-ssm's installer
# otherwise silently substitutes a prebuilt wheel that looks
# "compatible" by coarse tag matching but is actually ABI-
# mismatched, undefined symbol at import; MAMBA_FORCE_BUILD skips
# that shortcut in its own setup.py; CUDA_HOME must point at a
# toolkit whose major CUDA version matches torch's major CUDA
# version, or torch.utils.cpp_extension refuses to build at all.)
#
# Also seen on this host, unrelated to the above: uv's default
# hardlink-based install has silently dropped the actual .so payload
# of several unrelated nvidia-* packages (metadata installs fine,
# libraries are just missing from disk, surfacing later as an unrelated-
# looking ImportError: libFOO.so: cannot open shared object file). If
# that happens, `UV_LINK_MODE=copy uv pip install --no-cache --reinstall
# <package>==<pinned-version>` for the specific broken package fixes it;
# a version already matching the lock is otherwise skipped and won't
# self-heal, so check by actually importing torch, not just `uv sync`
# exiting 0.
cuda = [
"mamba-ssm==2.3.0",
# odyssey.models.backbones.hybrid imports einops (deferred, since it's
# only needed by the hybrid-Mamba backbone) -- TrainingConfig's own
# default is backbone="hybrid", so this is load-bearing for the
# ordinary default path, not an edge case. Found missing from this
# file entirely during the env-gpu hardening audit (2026-08-18).
"einops>=0.7.0",
]
# GEMINI database access (odyssey/data/gemini/). Optional: nobody but Amrit
# has a login on the GEMINI node, so most of this project never needs a
# Postgres driver at all — see docs/gemini.md.
gemini = [
"sqlalchemy>=2.1.2",
"psycopg2-binary>=2.9.13",
"pandas>=3.0.6",
# extract_meds.py streams MEDS rows straight to per-shard Parquet
# files (MedsShardWriter) -- pandas' own to_parquet needs an engine,
# and this module also uses pyarrow directly for streaming writes.
"pyarrow>=15.0.0",
# extract_meds.py's per-table transforms are vectorized polars, not
# pandas .iterrows() -- listed here too (not just in the base
# dependencies above) since run.sh's minimal extract-node install is
# `--no-deps` + this extra only, and skips the base dependency list
# entirely.
"polars>=1.44.2",
]
# TabICLv2 in-context-learning baseline (odyssey/inference/tabicl_baseline.py),
# a second comparator against the alerts evaluation's tuned GBM. Optional:
# nothing in the training/inference path depends on it, only that one
# baseline module, whose own import of `tabicl` is itself deferred to call
# time -- installing this extra is only needed to actually fit one.
tabicl = [
"tabicl>=2.2.0",
]
# InterpretML's ExplainableBoostingClassifier (odyssey/inference/ebm_baseline.py),
# a third comparator against the alerts evaluation: intrinsic, training-time
# interpretability (per-feature shape functions, pairwise interactions), unlike
# the GBM's post-hoc explainers or TabICL's zero-shot prediction. CPU-only,
# optional: nothing in the training/inference path depends on it.
# `interpret-core` (not the full `interpret` metapackage) deliberately: the
# full package's default extras pull in `interpret-core[shap]` -> `shap` ->
# a pinned old `llvmlite` that only supports Python <3.10, incompatible with
# this project's 3.12 pin. `interpret-core` alone still provides
# `interpret.glassbox.ExplainableBoostingClassifier`, the only thing this
# module imports.
ebm = [
"interpret-core>=0.6.0",
]
# SurvivalPFN (odyssey/inference/survivalpfn_baseline.py), a fourth comparator:
# a prior-data fitted network for right-censored survival analysis, fit
# survival-natively (hours to onset/censor, not a per-horizon binary outcome)
# the same way this project's own hazard heads are trained -- unlike the GBM/
# TabICL/EBM baselines' binary-cohort framing. Not on PyPI, git-only; not
# installed by default, nothing in the training/inference path depends on it.
# `omegaconf` pinned explicitly alongside the git dependency: it is a hard
# runtime import in their code (survivalpfn/models/loading.py) but only
# declared under their own `train` optional-extra, not their base
# dependencies -- a real packaging gap on their side, routed around here
# rather than fixed upstream.
survivalpfn = [
"survivalpfn @ git+https://github.com/rgklab/SurvivalPFN.git",
"omegaconf>=2.3.0",
]
# MEDS-Tab (odyssey/inference/meds_tab_baseline.py), a fifth comparator: the
# field-standard tabularization-plus-XGBoost protocol for generic MEDS
# datasets, run via its own CLI (a separate driver script, not this
# project's training/inference path) rather than reimplemented. On PyPI,
# no direct-reference/hatchling wrinkle the way the survivalpfn extra
# needed.
meds_tab = [
"meds-tab>=0.2.0",
]
# Frozen text-encoder embeddings for the notes sidecar
# (odyssey/text/embed_notes.py, Track A item 7): pooled hidden states from
# a Hugging Face encoder/decoder, PCA-reduced for the tuned GBM's alert
# features. The standing decision is MedGemma (google/medgemma-4b-it) on
# every site, with one dedicated embedder run alongside on MIMIC -- so
# this needs Gemma3/MedGemma support, not just any transformers version.
# scikit-learn (embed_notes.py's fit_pca) is already a base dependency
# above, not repeated here. accelerate is deliberately NOT included:
# load_encoder does its own model.to(device) placement, not
# from_pretrained(..., device_map=...), so nothing here actually needs
# accelerate's device-mapping machinery -- add it if that changes.
text = [
"transformers>=5.17.0",
"sentencepiece>=0.2.0",
"protobuf>=7.36.2",
]
# NOTE: lightning/etc. are intentionally omitted until the concept-
# bottleneck architecture actually needs them — add alongside the code
# that uses them, not speculatively.
[dependency-groups]
dev = [
"pytest>=9.0.3",
"pytest-cov>=6.0.0",
"pytest-asyncio>=0.25.2",
"pytest-mock>=3.16.0",
"pre-commit>=4.1.0",
"pip-audit>=2.7.3",
"codecov>=2.1.13",
"nbstripout>=0.6.1",
"mypy>=2.3.1",
"ruff>=0.16.10",
"nbqa[toolchain]>=1.9.1",
# pip>=26.1.2 fixes PYSEC-2026-196
"pip>=26.1.2",
# virtualenv>=21.7.13 fixes PYSEC-2026-4011, PYSEC-2026-4012, PYSEC-2026-4013, PYSEC-2026-4014
"virtualenv>=21.7.13",
]
docs = [
"mkdocs>=1.5.0",
"mkdocs-material>=9.0.0",
"mkdocstrings[python]>=1.0.6",
]
# Default dependency groups to be installed
[tool.uv]
default-groups = ["dev"]
[tool.uv.sources]
# See the torch==2.6.0 pin's comment above -- only the GCP A100 host
# (sys_platform == "linux") needs the cu124 build; Mac keeps plain PyPI.
torch = [
{ index = "pytorch-cu124", marker = "sys_platform == 'linux'" },
]
[[tool.uv.index]]
name = "pytorch-cu124"
url = "https://download.pytorch.org/whl/cu124"
explicit = true
[tool.mypy]
# Same reason as the ruff exclude above: these two ran on the VMs and
# are committed verbatim, so they cannot be annotated without editing
# the code that produced the published table.
exclude = "cohort_check_(mimic|eicu)\\.py$"
ignore_missing_imports = true
install_types = true
pretty = true
namespace_packages = true
explicit_package_bases = true
non_interactive = true
warn_unused_configs = true
allow_any_generics = false
allow_subclassing_any = false
allow_untyped_calls = false
allow_untyped_defs = false
allow_incomplete_defs = false
check_untyped_defs = true
allow_untyped_decorators = false
warn_redundant_casts = true
warn_unused_ignores = true
warn_return_any = true
implicit_reexport = false
strict_equality = true
extra_checks = true
# The cohort check scripts are committed verbatim as they ran on the VM.
# They cannot be annotated without redefining the published cohort groups.
# polars 1.44.2 tightened quantile()'s return type to float | None, which
# triggers new mypy errors in these files. The exclude above handles file
# discovery; this override silences errors when pre-commit passes the files
# explicitly on the command line (mypy ignores exclude for explicit args).
[[tool.mypy.overrides]]
module = [
"cohort_check_mimic",
"cohort_check_eicu",
"scripts.cohort.cohort_check_mimic",
"scripts.cohort.cohort_check_eicu",
]
ignore_errors = true
[tool.ruff]
include = ["*.py", "pyproject.toml", "*.ipynb"]
exclude = [
# Committed verbatim as they ran on the VMs: their marker regexes
# define the paper's published cohort groups, so reformatting them
# would silently redefine what that table measures.
"scripts/cohort/cohort_check_mimic.py",
"scripts/cohort/cohort_check_eicu.py",
]
line-length = 88
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
docstring-code-format = true
[tool.ruff.lint]
select = [
"A", # flake8-builtins
"B", # flake8-bugbear
"COM", # flake8-commas
"C4", # flake8-comprehensions
"RET", # flake8-return
"SIM", # flake8-simplify
"ICN", # flake8-import-conventions
"Q", # flake8-quotes
"RSE", # flake8-raise
"D", # pydocstyle
"E", # pycodestyle
"F", # pyflakes
"I", # isort
"W", # pycodestyle
"N", # pep8-naming
"ERA", # eradicate
"PL", # pylint
# pyupgrade, typing modernization only: builtin generics (dict/list/
# tuple over typing.Dict/...), X | None over Optional/Union, and the
# deprecated typing imports that go with them. requires-python is
# >= 3.12, so these are safe in runtime positions too.
"UP006", # non-pep585-annotation: use builtin generics
"UP007", # non-pep604-annotation-union: X | Y over Union[X, Y]
"UP035", # deprecated typing imports (typing.Dict, typing.List, ...)
"UP037", # remove quotes from type annotations that no longer need them
"UP045", # non-pep604-annotation-optional: X | None over Optional[X]
]
fixable = ["A", "B", "COM", "C4", "RET", "SIM", "ICN", "Q", "RSE", "D", "E", "F", "I", "W", "N", "ERA", "PL", "UP"]
ignore = [
"B905", # `zip()` without an explicit `strict=` parameter
"E501", # line too long
"D203", # 1 blank line required before class docstring
"D213", # Multi-line docstring summary should start at the second line
"PLR2004", # Replace magic number with named constant
"PLR0913", # Too many arguments
"COM812", # Missing trailing comma
"ERA001", # Found commented-out code (too many false positives with math comments)
"A001", # Ignore variable `input` is shadowing a Python builtin (common for torch)
"A002", # Ignore variable `input` is shadowing a Python builtin in function (common for torch)
"D301", # r-strings for docstrings with backslashes
]
# Ignore import violations in all `__init__.py` files.
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["E402", "F401", "F403", "F811"]
# Ignoring undocumented public functions, public init, magic method in tests folder
"tests/*" = ["D103", "D105", "D107"]
[tool.ruff.lint.pep8-naming]
ignore-names = ["X*", "setUp"]
[tool.ruff.lint.isort]
lines-after-imports = 2
[tool.ruff.lint.pydocstyle]
convention = "numpy"
[tool.ruff.lint.pycodestyle]
max-doc-length = 88
[tool.pytest.ini_options]
markers = [
"integration_test: marks tests as integration tests",
]
[tool.coverage]
[tool.coverage.run]
source=["odyssey", "apps"]
omit=["tests/*", "*__init__.py"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
# Required because the `survivalpfn` extra above is a direct git reference
# (not on PyPI) -- hatchling refuses to build metadata for a project with
# any direct-reference dependency unless this is set, even though the
# reference is confined to one optional extra nothing else depends on.
allow-direct-references = true