diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 410bcd8..9ca25ef 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,22 +1,9 @@ -## What does this PR do? +## What does this change? - + -Fixes #\ +## Checklist -## Before submitting - -- [ ] Did you make sure **title is self-explanatory** and **the description concisely explains the PR**? -- [ ] Did you make sure your **PR does only one thing**, instead of bundling different changes together? -- [ ] Did you list all the **breaking changes** introduced by this pull request? -- [ ] Did you **test your PR locally** with `pytest` command? -- [ ] Did you **run pre-commit hooks** with `pre-commit run -a` command? - -## Did you have fun? - -Make sure you had fun coding ๐Ÿ™ƒ +- [ ] `pytest` passes locally +- [ ] `pre-commit run -a` passes +- [ ] Documentation and `CHANGELOG.md` updated where behaviour changed diff --git a/.github/codecov.yml b/.github/codecov.yml index c66853c..3778238 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -1,15 +1,8 @@ coverage: status: - # measures overall project coverage project: default: - threshold: 100% # how much decrease in coverage is needed to not consider success - - # measures PR or single commit coverage + threshold: 2% # allowed drop in total coverage patch: default: - threshold: 100% # how much decrease in coverage is needed to not consider success - - - # project: off - # patch: off + target: 70% diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5a861fd..ca8e216 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,16 +1,13 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates - version: 2 updates: - - package-ecosystem: "pip" # See documentation for possible values - directory: "/" # Location of package manifests + - package-ecosystem: "pip" + directory: "/" schedule: - interval: "daily" + interval: "weekly" ignore: - - dependency-name: "pytorch-lightning" - update-types: ["version-update:semver-patch"] - - dependency-name: "torchmetrics" + - dependency-name: "lightning" update-types: ["version-update:semver-patch"] + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml deleted file mode 100644 index 59af159..0000000 --- a/.github/release-drafter.yml +++ /dev/null @@ -1,44 +0,0 @@ -name-template: "v$RESOLVED_VERSION" -tag-template: "v$RESOLVED_VERSION" - -categories: - - title: "๐Ÿš€ Features" - labels: - - "feature" - - "enhancement" - - title: "๐Ÿ› Bug Fixes" - labels: - - "fix" - - "bugfix" - - "bug" - - title: "๐Ÿงน Maintenance" - labels: - - "maintenance" - - "dependencies" - - "refactoring" - - "cosmetic" - - "chore" - - title: "๐Ÿ“๏ธ Documentation" - labels: - - "documentation" - - "docs" - -change-template: "- $TITLE @$AUTHOR (#$NUMBER)" -change-title-escapes: '\<*_&' # You can add # and @ to disable mentions - -version-resolver: - major: - labels: - - "major" - minor: - labels: - - "minor" - patch: - labels: - - "patch" - default: patch - -template: | - ## Changes - - $CHANGES diff --git a/.github/workflows/code-quality-main.yaml b/.github/workflows/code-quality-main.yaml deleted file mode 100644 index 925ef03..0000000 --- a/.github/workflows/code-quality-main.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# Same as `code-quality-pr.yaml` but triggered on commit to main branch -# and runs on all files (instead of only the changed ones) - -name: Code Quality Main - -on: - push: - branches: [main] - -jobs: - code-quality: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v5 - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.12" - - - name: Install pre-commit - run: python -m pip install --upgrade pip pre-commit - - - name: Run pre-commit on all files - run: pre-commit run --all-files --show-diff-on-failure diff --git a/.github/workflows/code-quality-pr.yaml b/.github/workflows/code-quality-pr.yaml deleted file mode 100644 index 76161a6..0000000 --- a/.github/workflows/code-quality-pr.yaml +++ /dev/null @@ -1,44 +0,0 @@ -# This workflow finds which files were changed, prints them, -# and runs `pre-commit` on those files. - -# Inspired by the sktime library: -# https://github.com/alan-turing-institute/sktime/blob/main/.github/workflows/test.yml - -name: Code Quality PR - -on: - pull_request: - branches: [main, "release/*", "dev"] - -jobs: - code-quality: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v5 - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.12" - - - name: Find modified files - id: file_changes - uses: trilom/file-changes-action@v1.2.4 - with: - output: " " - - - name: List modified files - run: echo '${{ steps.file_changes.outputs.files}}' - - - name: Install pre-commit - run: python -m pip install --upgrade pip pre-commit - - - name: Run pre-commit on changed files - run: | - if [ -z "${{ steps.file_changes.outputs.files }}" ]; then - echo "No changed files detected by file-changes action; skipping pre-commit." - exit 0 - fi - pre-commit run --files ${{ steps.file_changes.outputs.files }} --show-diff-on-failure diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000..7f00ed4 --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,18 @@ +name: Code quality + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - run: python -m pip install --upgrade pip pre-commit + - run: pre-commit run --all-files --show-diff-on-failure diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml deleted file mode 100644 index 6a45e15..0000000 --- a/.github/workflows/release-drafter.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Release Drafter - -on: - push: - # branches to consider in the event; optional, defaults to all - branches: - - main - -permissions: - contents: read - -jobs: - update_release_draft: - permissions: - # write permission is required to create a github release - contents: write - # write permission is required for autolabeler - # otherwise, read permission is required at least - pull-requests: write - - runs-on: ubuntu-latest - - steps: - # Drafts your next Release notes as Pull Requests are merged into "master" - - uses: release-drafter/release-drafter@v5 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dda0175..ac8acf3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,42 +4,32 @@ on: push: branches: [main] pull_request: - branches: [main, "release/*", "dev"] + branches: [main] env: - # Bump these together. torch-scatter/torch-sparse are only published for a - # subset of torch releases: https://data.pyg.org/whl/ has no wheels beyond - # torch 2.12, so raising TORCH_VERSION past it means those extensions would - # have to build from source, which fails on macOS (PyTorch's own - # c10/util/strong_type.h specializes std::is_arithmetic, which recent libc++ - # rejects). Check the index before changing this. - TORCH_VERSION: "2.12.0" - PYTHON_VERSION: "3.12" + # torch-sparse is published at https://data.pyg.org/whl/ for a subset of + # torch releases only; check the index before raising this. + TORCH_VERSION: "2.4.1" jobs: - run-tests: - name: Pytest (${{ matrix.os }}) + test: + name: Pytest (${{ matrix.os }}, Python ${{ matrix.python }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] + include: + - { os: ubuntu-latest, python: "3.11" } + - { os: ubuntu-latest, python: "3.12" } + - { os: macos-latest, python: "3.12" } timeout-minutes: 45 - steps: - - name: Checkout - uses: actions/checkout@v5 - - - name: Set up Python - uses: actions/setup-python@v6 + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 with: - python-version: ${{ env.PYTHON_VERSION }} + python-version: ${{ matrix.python }} cache: pip - - # Linux resolves CUDA builds by default, which are ~3GB and useless on a - # CPU runner. macOS wheels on PyPI are already CPU/MPS only, and the - # download.pytorch.org cpu index does not publish macOS builds. - - name: Install PyTorch (CPU) + - name: Install PyTorch (CPU) and torch-sparse run: | python -m pip install --upgrade pip if [ "$RUNNER_OS" = "Linux" ]; then @@ -47,111 +37,12 @@ jobs: else pip install "torch==${TORCH_VERSION}" fi - - # Prebuilt binaries. Never let these build from source; see the note on - # TORCH_VERSION above. - - name: Install PyG compiled extensions - run: | - pip install torch-scatter torch-sparse \ - --only-binary :all: \ + pip install torch-scatter torch-sparse --only-binary :all: \ -f "https://data.pyg.org/whl/torch-${TORCH_VERSION}+cpu.html" - - name: Install the package - run: pip install ".[interpretability,io,stats,plots]" pytest - - - name: Show key package versions - run: | - python --version - python - <<'PY' - import importlib - - checks = [ - "torch", - "torch_geometric", - "torch_scatter", - "torch_sparse", - "lightning", - "pytorch_lightning", - "torchmetrics", - "omegaconf", - "hydra", - "numpy", - "pandas", - "scipy", - "sklearn", - "plotly", - ] - for name in checks: - mod = importlib.import_module(name) - print(f"{name}=={getattr(mod, '__version__', 'unknown')}") - PY - - - name: Run pytest - run: pytest -v - - code-coverage: - runs-on: ubuntu-latest - timeout-minutes: 45 - - steps: - - name: Checkout - uses: actions/checkout@v5 - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: ${{ env.PYTHON_VERSION }} - cache: pip - - - name: Install PyTorch (CPU) and PyG extensions - run: | - python -m pip install --upgrade pip - pip install "torch==${TORCH_VERSION}" --index-url https://download.pytorch.org/whl/cpu - pip install torch-scatter torch-sparse \ - --only-binary :all: \ - -f "https://data.pyg.org/whl/torch-${TORCH_VERSION}+cpu.html" - - - name: Run tests and collect coverage - run: | - pip install ".[interpretability,io,stats,plots]" pytest "pytest-cov[toml]" - pytest --cov grass_mil - - - name: Upload coverage to Codecov + run: pip install ".[io,test]" + - name: Run the tests + run: pytest --cov grass_mil --cov-report xml + - name: Upload coverage + if: matrix.os == 'ubuntu-latest' && matrix.python == '3.12' uses: codecov/codecov-action@v5 - - packaging: - name: Install from source and run the CLI - runs-on: ubuntu-latest - timeout-minutes: 30 - - steps: - - name: Checkout - uses: actions/checkout@v5 - - # Deliberately the oldest supported Python, so `requires-python` is - # exercised at its floor rather than only at the version CI happens to use. - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: "3.11" - cache: pip - - # A plain, unpinned pip install: this is the path a downstream user takes, - # and it is the one that silently rots when the package layout or the - # config tree moves. It deliberately does NOT pin torch, so a dependency - # that breaks on the latest release shows up here first. - - name: Install the package - run: pip install . - - - name: Entrypoints resolve packaged configs from an unrelated directory - run: | - mkdir -p "${RUNNER_TEMP}/elsewhere" - cd "${RUNNER_TEMP}/elsewhere" - grass-mil-train --help - grass-mil-report --help - python -c "import grass_mil, grass_mil.configs; print(grass_mil.__file__)" - - - name: Packaging contract tests - run: | - pip install pytest - pytest -v tests/test_packaging.py diff --git a/.gitignore b/.gitignore index 69d0e65..d446e47 100644 --- a/.gitignore +++ b/.gitignore @@ -127,12 +127,8 @@ dmypy.json # Pyre type checker .pyre/ -### VisualStudioCode -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json +### Editors +.vscode/ *.code-workspace # JetBrains @@ -143,17 +139,11 @@ dmypy.json *.tar *.tar.gz -# Lightning-Hydra-Template -configs/local/default.yaml -src/grass_mil/configs/local/*.yaml -!src/grass_mil/configs/local/.gitkeep +# Run outputs and local data /data/ /logs/ .env -# Aim logging -.aim - # Vignette scratch space (downloaded data, graphs, runs) vignettes/vignette_work/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 72b1c6c..76b0314 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - id: check-added-large-files - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.9 + rev: v0.8.6 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/.project-root b/.project-root deleted file mode 100644 index 63eab77..0000000 --- a/.project-root +++ /dev/null @@ -1,2 +0,0 @@ -# this file is required for inferring the project root directory -# do not delete diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 893f70e..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll.ruff": "always", - "source.organizeImports.ruff": "always" - }, - "[python]": { - "editor.defaultFormatter": "charliermarsh.ruff" - }, - "ruff.format.args": [ - "--line-length=99" - ] -} diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fa2e37..40b703c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,30 +1,38 @@ # Changelog -## 1.0.0 (2026-10) - -Public release accompanying the preprint -[GRASS-MIL: graph-based representation and discovery of phenotype-associated spatial structures with multiple instance learning](https://www.biorxiv.org/content/10.64898/2026.10.08.757722v1). - -### Method (defaults match the preprint; every setting stays configurable) - -- Delaunay cell graph with the bounded log edge length `clip((ln d - 2)/3, 0, 1)` as the edge feature (`log_distance`). -- GINE ego-graph encoder (4 x 256, learnable epsilon, layer norm, max pooling, xavier-initialised cell-type embedding) as `model=grass_mil`. -- Projected gated attention with one channel per class (32 / 8, kaiming) and logit-space pooling, so every bag logit is an exact sum of per-cell contributions. -- One region = one bag: all sampled ego-graphs of a region in one batch, one attention softmax, one loss; region-level validation metrics and selection. -- Training regimes as presets (`regime=mil|mil_frozen|linear_probehead|linear_ftall|linear_ftall_reg`) with per-component learning-rate scales, head pre-fit by a logistic probe, class weights from the training split, per-step cosine schedule, gradient accumulation over regions, AMP-safe scheduler stepping, elementwise feature drop with an unassigned token. -- BGRL "A3" pretraining preset (`regime=pretrain_a3`): roots drawn from the union of all regions, momentum 0.99 -> 1.0, DropImportance + feature noise, checkpoint every 5000 steps. -- Import of legacy SPACE-GM style BGRL checkpoints (`model.encoder_init_map=spacegm_bgrl`). +## 1.1.0 (2026-10) + +A package-level cleanup. Commands, configuration keys and file formats changed; +graphs and checkpoints of 1.0.0 need to be rebuilt. + +- **Regimes renamed**; `finetune` is the default: `linear_ftall` โ†’ `finetune`, + `linear_ftall_reg` โ†’ `finetune_reg`, `linear_probehead` โ†’ `attention_only`, + `mil` โ†’ `mlp_head`, `mil_frozen` โ†’ `mlp_head_frozen`, `pretrain_a3` โ†’ + `pretrain`. The probe head is fitted automatically; `grass-mil-probe` is gone. +- **Niche analysis** (`grass-mil-niches`) reports the primary results directly: + niches in the attention space, composition, association, margin with + intervals, per-niche Moran's I, neighbourhood enrichment, cross-L and contact + enrichment, and optional survival. `grass-mil-metaclusters` is optional. +- **Removed**: `grass-mil-report` and its plugin stack, `grass-mil-eval`, + `grass-mil-loocv`, `grass-mil-scores`, mean pooling, the GCN/GAT/GraphSAGE/GIN + encoders, per-sample PCA, `finetune.freeze_head`, the 1.0.0 checkpoint-import + options and unused logger, debug and multi-GPU presets. +- **Configuration**: one `data.input` block for all formats, `data.unit` + (`region`, `tile`, `polygon`), labels attached at load time (one set of + graphs for every task and fold), `task` (`classification`, `regression`, + `survival`) with its own selection metric. +- **Fixes**: gradient clipping under mixed precision now acts on unscaled + gradients; cross-validation refits no longer evaluate on the held-out fold; + `grass-mil-predict` reports AUROC and works without `regime=`; the PCA + feature reducer works; survival training optimises over several units per + step; numeric sample ids; stale processed graphs are detected. +- **Statistics**: the Cox loss handles tied times (Breslow) and survival heads + start from a Cox probe; outer folds must keep patients together; refits keep + the inner runs' learning-rate schedule; margin intervals resample patients; + per-niche cross-L is relative to random labelling and Moran's I is tested + with exact randomisation moments; prediction checks that the cohort was + preprocessed like the training data. -### Analyses - -- `grass-mil-probe`, `grass-mil-nested-cv`: nested patient-grouped cross-validation with explicit fold files, inner-fold epoch selection, refit and out-of-fold predictions (region and patient level). -- `grass-mil-extract`: per-cell cache (embeddings, instance logits, attention) of trained models over every cell. -- `grass-mil-niches`: niche discovery on a model's cells (cosine HDBSCAN by default, any registry backend), attention-weighted margins, per-niche tables, co-localisation, figures. -- `grass-mil-metaclusters`: cross-fold niche replication (constrained matching on composition), whole-dataset transfer, shared label-free basis. -- `grass-mil-scores`: bootstrap AUROC intervals, DeLong tests, within-fold standardisation, operating points (sensitivity, specificity, PPV, NPV, likelihood ratios). -- `grass-mil-spatial-stats`: neighbourhood enrichment, cellular neighbourhoods, contact fractions and cross-K per region with patient-level group tests. -- Patient-level aggregation in `grass-mil-predict` (mean of region probabilities or one merged bag). - -### Housekeeping +## 1.0.0 (2026-10) -- Removed stale component configs, the dummy dataset, the embeddings shim and internal audit notes; added `CITATION.cff`; `hydra-core` 1.3.5+. +Public release accompanying the preprint. diff --git a/CITATION.cff b/CITATION.cff index e12d63c..eabc2db 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -2,8 +2,8 @@ cff-version: 1.2.0 message: "If you use GRASS-MIL, please cite the preprint." title: "GRASS-MIL: graph-based representation and discovery of phenotype-associated spatial structures with multiple instance learning" type: software -version: 1.0.0 -date-released: 2026-10-13 +version: 1.1.0 +date-released: 2026-10-15 license: MIT repository-code: https://github.com/BoevaLab/grass-mil authors: diff --git a/Makefile b/Makefile index 801692b..7ce5ae7 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,15 @@ -help: ## Show help - @grep -E '^[.a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' +help: ## Show the targets + @grep -E '^[a-z-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-12s\033[0m %s\n", $$1, $$2}' -clean: ## Remove caches and build artefacts - rm -rf dist - find . -type f -name "*.DS_Store" -ls -delete - find . | grep -E "(__pycache__|\.pyc|\.pyo)" | xargs rm -rf - find . | grep -E ".pytest_cache" | xargs rm -rf - find . | grep -E ".ipynb_checkpoints" | xargs rm -rf - rm -f .coverage +test: ## Run the tests (fast ones only) + pytest -m "not slow" -clean-logs: ## Remove run outputs under logs/ - rm -rf logs/** +test-all: ## Run every test, including the end-to-end workflow + pytest -format: ## Run pre-commit hooks (ruff lint + format) +format: ## Lint and format (pre-commit hooks) pre-commit run -a -test: ## Run the test suite - pytest +clean: ## Remove caches and build artefacts + rm -rf dist build .pytest_cache .coverage + find . -name __pycache__ -type d -prune -exec rm -rf {} + diff --git a/README.md b/README.md index 65c0f5e..51caad9 100644 --- a/README.md +++ b/README.md @@ -7,40 +7,30 @@ GRASS-MIL predicts a patient- or region-level phenotype from cell-resolved spatial omics data (multiplexed imaging, spatial transcriptomics with cell -types) and tells you *which spatial structures* carry the prediction. It +types) and shows *which spatial structures* carry the prediction. It represents every cell by its spatial neighbourhood, learns those representations without labels, pools them with per-class attention into a -bag prediction whose every logit is an exact sum of per-cell contributions, -and turns the contributions into **niches**: recurrent cellular -neighbourhoods with an association to the phenotype and an attribution by -the model, checked for replication across cross-validation folds. - -This repository is the public implementation accompanying the preprint: - -> Rabuzin L, Yates J, Xu M, Boeva V. *GRASS-MIL: graph-based representation -> and discovery of phenotype-associated spatial structures with multiple -> instance learning.* bioRxiv (2026). -> doi: [10.64898/2026.10.08.757722](https://www.biorxiv.org/content/10.64898/2026.10.08.757722v1) +prediction whose every logit is an exact sum of per-cell contributions, and +turns the contributions into **niches**: recurrent cellular neighbourhoods, +each with its composition, its association with the phenotype, the model's +attribution to it and its spatial organisation. ## The method in one paragraph -Cells of a region form a Delaunay graph whose edges carry the bounded log -edge length. A GINE graph network encodes the *k*-hop ego-graph around every -cell (4 hops, width 256 by default) and is pretrained with BGRL on all regions, -label-free. For prediction, the ego-graphs of a region are the instances of -one bag: an instance head gives per-class logits, a projected gated attention -net gives per-class weights, and the bag logit is `L_c = sum_i A[i,c] * l[i,c]` -(logit-space pooling), so every cell's contribution `A[i,c] (l[i,c] - b_c)` is -exact rather than approximated. Fine-tuning regimes range from attention-only -on a frozen encoder with a probe-fitted linear head to end-to-end fine-tuning. -Evaluation is a nested, patient-grouped cross-validation. Interpretation -clusters the held-out cells of each fold model in its own embedding space -(cosine HDBSCAN by default), scores every cluster by its empirical association -and its attention-weighted margin, and matches clusters across folds into -metaclusters that replicate. Every setting above is a default of a Hydra -config, not a constant: depth, width, ego-graph hops, node and edge features, -attention sizes, learning rates, bag sizes, schedules, augmentations, fold -counts and clustering settings are all overridable from the command line. +Cells of a region form a Delaunay graph whose edges carry the bounded log edge +length. A GINE graph network encodes the *k*-hop ego-graph around every cell +(4 hops, width 256) and is pretrained with BGRL on all regions, label-free. For +prediction, the ego-graphs of a region are the instances of one bag: an +instance head gives per-class logits, a projected gated attention gives +per-class weights, and the bag logit is `L_c = sum_i A[i,c] * l[i,c]`, so every +cell's contribution `A[i,c] (l[i,c] - b_c)` is exact rather than approximated. +By default the pretrained encoder is fine-tuned end to end with a linear +instance head initialised by a probe on its frozen embeddings. Evaluation is a +nested, patient-grouped cross-validation. Interpretation clusters the held-out +cells of a model in its attention space (cosine HDBSCAN) and describes every +niche by its composition, empirical association, attention-weighted margin and +classical spatial statistics within the niche. Every setting is a key of the +[configuration](docs/configuration.md) and can be changed from the command line. ## Install @@ -48,151 +38,144 @@ counts and clustering settings are all overridable from the command line. git clone https://github.com/BoevaLab/grass-mil.git cd grass-mil python -m venv .venv && source .venv/bin/activate -pip install -e ".[interpretability,io,stats,plots]" +pip install -e ".[io]" +# torch-sparse for your PyTorch version (ego-graph sampling runs on the CPU, so the +# CPU wheel serves CUDA builds of PyTorch as well): +pip install torch-sparse -f https://data.pyg.org/whl/torch-$(python -c "import torch; print(torch.__version__.split('+')[0])")+cpu.html ``` -`torch-geometric` needs the compiled `torch-sparse` / `torch-scatter` wheels -matching your PyTorch build for the ego-graph sampler (see -[`.github/workflows/test.yml`](.github/workflows/test.yml) for the exact pip -lines used in CI, or use [`environment.yaml`](environment.yaml) with conda). -Extras: `interpretability` (UMAP, plotly, matplotlib), `io` (h5ad, polygons), -`stats` (statsmodels), `plots`, `report` (PDF rendering), `spatial-stats` -(squidpy backend), `sce` (R inputs). - -Installing exposes the CLI entry points below; the Hydra config tree ships -inside the package, so every command works from any directory. +or `conda env create -f environment.yaml`. Extras: `io` (h5ad inputs, polygon +units), `sce` (SingleCellExperiment inputs; also needs R with Bioconductor), +`test`. -## Quick start (public demo data, CPU, a few minutes) +## Quick start -The vignettes run the whole chain on `squidpy.datasets.mibitof()` (three -MIBI-TOF images, ~3,300 cells): +The [vignettes](vignettes/) run the whole workflow on a public MIBI-TOF dataset +in a few minutes (about ten on a CPU): ```bash pip install squidpy jupyter jupyter lab vignettes/ ``` -1. [`01_anndata_to_spatial_graphs.ipynb`](vignettes/01_anndata_to_spatial_graphs.ipynb): AnnData -> manifest -> cellular graphs. -2. [`02_pretraining_and_niches.ipynb`](vignettes/02_pretraining_and_niches.ipynb): BGRL pretraining (`regime=pretrain_a3`), fine-tuning (`regime=mil`), instance export, interpretability report. -3. [`03_nested_cv_niches_and_metaclusters.ipynb`](vignettes/03_nested_cv_niches_and_metaclusters.ipynb): nested CV, cell extraction, niches, metaclusters, scores, spatial statistics. +## Your first cohort + +Work in a project folder of your own and run every command from it: + +```text +my_project/ + data/ cell tables, manifest.csv, labels.csv, folds.json + configs/experiment/my_cohort.yaml + runs/ one output folder per step +``` -## Typical usage on your own cohort +**1. Prepare the data**: one cell table per region (CSV, h5ad or +SingleCellExperiment), a manifest listing them, a label file and the +cross-validation folds. [Data preparation](docs/data_preparation.md) describes +the formats and has short scripts for the manifest and the folds. -Describe the cohort once (copy -[`configs/experiment/example_cohort.yaml`](src/grass_mil/configs/experiment/example_cohort.yaml) -to `my_cohort.yaml`, fill in paths and column names), then: +**2. Describe the cohort**: copy the template into `configs/experiment/` and +fill in the column names (the data are looked up in `./data`): ```bash -grass-mil-train experiment=my_cohort +trainer.fast_dev_run=true # build the graphs -grass-mil-train experiment=my_cohort regime=pretrain_a3 \ - paths.output_dir=runs/pretrain_a3 trainer=gpu # pretrain, label-free -grass-mil-nested-cv experiment=my_cohort regime=linear_ftall_reg \ - model.init_from_ckpt=runs/pretrain_a3/checkpoints/last.ckpt \ - paths.output_dir=runs/nested trainer=gpu # nested CV, OOF scores -grass-mil-scores input=runs/nested/oof_predictions.csv output_dir=runs/scores # CIs, operating points -grass-mil-extract experiment=my_cohort regime=linear_ftall_reg \ - extract.nested_cv_summary=runs/nested/nested_cv_summary.json \ - extract.output_dir=runs/cells # every cell, every fold model -grass-mil-niches cells_dir=runs/cells/fold2 output_dir=runs/niches_fold2 # niches of one model -grass-mil-metaclusters cells_dir=runs/cells output_dir=runs/meta labels.label_file=... \ - labels.label_column=STAS labels.patient_column=PATIENT # replication across folds -grass-mil-spatial-stats cells_dir=runs/cells/fold0 output_dir=runs/spatial labels.label_file=... \ - labels.label_column=STAS labels.patient_column=PATIENT reference=[tumor] # model-free statistics +mkdir -p configs/experiment +python -c "import grass_mil.configs as c, pathlib, shutil; shutil.copy(pathlib.Path(c.__file__).parent / 'experiment/example_cohort.yaml', 'configs/experiment/my_cohort.yaml')" ``` -Step by step, with inputs, outputs and the variants (one split, tiles as -units, h5ad inputs, patient-level prediction): -[`docs/getting_started.md`](docs/getting_started.md). How configuration works -and how to change any setting: [`docs/configuration_guide.md`](docs/configuration_guide.md). - -## Workflows - -Inputs are a manifest CSV (`sample_id,input_path,input_type[,region_id,polygons_path]`), -per-sample cell tables (CSV/TSV, h5ad, SingleCellExperiment), a label CSV with a -unit-id column, a target column and a patient column, and a fold file (JSON list -of lists of unit ids). See [`docs/dataset_preparation.md`](docs/dataset_preparation.md). - -| Step | Command | Preset / keys | Outputs | Doc | -|---|---|---|---|---| -| Build graphs | `grass-mil-train data=... +trainer.fast_dev_run=true` (or any run) | `data.graph_builder.kwargs.edge_features=[log_distance,neighbor]`, `data.sample_unit=full` | `processed_index.json`, graph `.pt` files | [dataset_preparation](docs/dataset_preparation.md) | -| Pretrain the encoder | `grass-mil-train experiment=my_cohort regime=pretrain_a3` | `model=bgrl_a3`, `task=pretrain_bgrl_a3`, `data/sampler=ego_graph_pretrain` | `checkpoints/last.ckpt` | [workflows](docs/workflows_train_infer.md) | -| Fine-tune one split | `grass-mil-train regime=mil data=... model.init_from_ckpt=...` | `regime=mil|mil_frozen|linear_probehead|linear_ftall|linear_ftall_reg`, `data.split.explicit.*` | checkpoints, `metrics_per_epoch.jsonl`, `test_predictions.csv` | [nested_cv](docs/nested_cv.md) | -| Probe head | `grass-mil-probe regime=linear_probehead data=...` | `probe.*` | `probe_head.pt` | [nested_cv](docs/nested_cv.md) | -| Nested cross-validation | `grass-mil-nested-cv regime=... nested_cv.folds_file=folds.json` | `nested_cv.*` | `nested_cv_summary.json`, `oof_predictions.csv`, `oof_patient_predictions.csv` | [nested_cv](docs/nested_cv.md) | -| Predict / aggregate | `grass-mil-predict ckpt_path=... regime=...` | `aggregation.bag_scope=patient aggregation.mode=mean_probability|attention_weighted` | `predictions.csv`, `metrics.json`, instance tables | [inference](docs/inference_utility_suite.md) | -| Extract cells | `grass-mil-extract regime=... extract.nested_cv_summary=...` | `extract.*` | `cells//.npz` | [niche_discovery](docs/niche_discovery.md) | -| Niches of one model | `grass-mil-niches cells_dir=cells/fold2` | `reduction.*`, `clustering.*`, `sample_size`, `assign_k` | `clusters.csv`, `cells.csv`, `report.md`, figures | [niche_discovery](docs/niche_discovery.md) | -| Cross-fold replication | `grass-mil-metaclusters cells_dir=cells labels.label_file=...` | `stage`, `match.*`, `transfer.*`, `basis.*` | `metaclusters.csv`, patient shares and tests, transfer and basis reports | [niche_discovery](docs/niche_discovery.md) | -| Evaluation statistics | `grass-mil-scores input=oof_predictions.csv` | `scores`, `strata`, `operating_rules` | AUROC CIs, DeLong, operating points | [evaluation_stats](docs/evaluation_stats.md) | -| Classical spatial statistics | `grass-mil-spatial-stats cells_dir=... reference=[tumor]` | `analyses`, `group_map`, `nhood.*`, `cn.*`, `crossk.*` | neighbourhood enrichment, cellular neighbourhoods, contacts, cross-K tests | [spatial_statistics](docs/spatial_statistics.md) | -| Interpretability report | `grass-mil-report data.instance_table=...` | `clustering.*`, plugins | HTML / PDF report | [interpretability_suite](docs/interpretability_suite.md) | - -## Running the analyses of the preprint - -The preprint's numbers come from this chain with the default settings: - -1. graphs with `edge_features=[log_distance,neighbor]` and the cohort's native - coordinate units (`neighbor_cutoff_um` is the contact distance in those units); -2. `regime=pretrain_a3` on every region, label-free; -3. `grass-mil-nested-cv` with patient-grouped outer folds, inner `k=3`, the - regimes `linear_probehead` ("probehead"), `linear_ftall` ("ftALL"), - `linear_ftall_reg` ("ftALL, lr x0.3 + regularisation") and `mil`; - out-of-fold region scores, patient scores as the mean of a patient's regions - (merged bags via `grass-mil-predict`); -4. `grass-mil-scores` for bootstrap intervals, per-fold means, DeLong - comparisons and operating points; -5. `grass-mil-extract` of the fold refits, `grass-mil-niches` on each model's - held-out cells (PCA-10, L2, HDBSCAN mcs 300 / min_samples 1 / eom, 15-NN - assignment), `grass-mil-metaclusters` (Hellinger matching with cannot-link - within fold, transfer to all regions, shared k-means basis); -6. `grass-mil-spatial-stats` for the model-free comparison. - -[`docs/method_correspondence.md`](docs/method_correspondence.md) maps every -section of the preprint to the preset, module and test that implements it and -records the validation against the original runs. - -### Changing the defaults +Every command that reads the cohort gets `--config-dir configs +experiment=my_cohort`, and `hydra.run.dir` names the folder a step writes to. + +**3. Run the steps** (`trainer=gpu` uses a GPU; drop it to run on the CPU): ```bash -# shallower encoder, 3-hop ego-graphs, cell type only (no size feature) -grass-mil-nested-cv regime=mil model.encoder.num_layers=3 model.encoder.hidden_dim=128 \ - model.encoder.out_dim=128 data.sampler.runtime.depth=3 data.csv.molecular_columns=[] ... +# Pretrain the encoder on every region, without labels; the first command +# also builds the cell graphs (200 000 steps, about an hour on a GPU). +grass-mil-train --config-dir configs experiment=my_cohort regime=pretrain trainer=gpu \ + hydra.run.dir=runs/pretrain -# radius-capped ego-graphs and a different contact distance -grass-mil-train regime=mil data.sampler.runtime.ego_radius.enabled=true \ - data.sampler.runtime.ego_radius.radius_per_hop=30 data.graph_builder.kwargs.neighbor_cutoff_um=25 ... +# Evaluate by nested cross-validation (one model per outer fold). +grass-mil-nested-cv --config-dir configs experiment=my_cohort trainer=gpu \ + model.init_from_ckpt=runs/pretrain/checkpoints/last.ckpt hydra.run.dir=runs/nested_cv -# more roots per bag, longer schedule, five inner folds -grass-mil-nested-cv regime=linear_ftall data.sampler.runtime.subsample_count=512 \ - trainer.max_epochs=40 nested_cv.inner_k=5 ... +# Score every cell with the fold models. +grass-mil-extract --config-dir configs experiment=my_cohort \ + extract.nested_cv_summary=runs/nested_cv/nested_cv_summary.json hydra.run.dir=runs/extract + +# Niches of fold model 0, on the regions it did not train on. +grass-mil-niches cells_dir=runs/extract/cells/fold0 hydra.run.dir=runs/niches_fold0 +``` -# niches with k-means instead of HDBSCAN, 20 PCs, no sampling -grass-mil-niches cells_dir=... clustering.method=kmeans clustering.n_clusters=12 \ - reduction.n_components=20 sample_size=null +| Folder | What to look at | +|---|---| +| `runs/pretrain` | `checkpoints/last.ckpt`, the pretrained encoder | +| `runs/nested_cv` | `nested_cv_summary.json`: the metric of every outer fold and their mean ยฑ s.d., the number to report; `oof_predictions.csv`: every region scored by the model that held it out | +| `runs/extract/cells/fold` | per-cell outputs of the model of outer fold `i` | +| `runs/niches_fold0` | `report.md`, `niches.csv`, `descriptors/`, `figures/` | + +Each command ends by printing where it wrote. With a few dozen regions, see +[small cohorts](docs/training_and_evaluation.md#small-cohorts) first. + +**Optional steps**, with the same conventions: + +```bash +# Niches that recur across the fold models +grass-mil-metaclusters cells_dir=runs/extract/cells hydra.run.dir=runs/metaclusters +# Classical spatial statistics compared between classes, without a model +grass-mil-spatial-stats cells_dir=runs/extract/cells/fold0 hydra.run.dir=runs/spatial_stats +# Score a new cohort (its own cohort file) with one trained model +grass-mil-predict --config-dir configs experiment=new_cohort \ + ckpt_path=runs/nested_cv/outer_0/refit/checkpoints/last.ckpt hydra.run.dir=runs/predict ``` -Every key is documented in [`docs/hyperparameter_reference.md`](docs/hyperparameter_reference.md). +A single model on all labelled regions (`grass-mil-train --config-dir configs +experiment=my_cohort model.init_from_ckpt=runs/pretrain/checkpoints/last.ckpt`) +uses a random patient-grouped split for validation and testing. + +## Changing the defaults + +Any setting can be changed on the command line (`key=value`); the examples +leave out `--config-dir configs` and `hydra.run.dir`. + +```bash +# another fine-tuning regime: slower encoder with regularisation +grass-mil-nested-cv experiment=my_cohort regime=finetune_reg model.init_from_ckpt=... + +# a shallower encoder on 3-hop ego-graphs, cell type only (no continuous feature) +grass-mil-train experiment=my_cohort regime=pretrain model.encoder.num_layers=3 \ + data.sampler.depth=3 model.encoder.hidden_dim=128 model.encoder.out_dim=128 data.input.features=null + +# radius-capped ego-graphs and another contact distance +grass-mil-train experiment=my_cohort data.sampler.ego_radius.enabled=true \ + data.sampler.ego_radius.radius_per_hop=30 data.graph.neighbor_cutoff=25 ... + +# more cells per bag, a longer schedule, five inner folds +grass-mil-nested-cv experiment=my_cohort data.sampler.roots_per_unit=512 \ + trainer.max_epochs=40 nested_cv.inner_k=5 ... + +# survival instead of classification +grass-mil-nested-cv experiment=my_cohort task=survival task.target_columns=[time,event] ... + +# niches in the encoder embedding with k-means +grass-mil-niches cells_dir=... clustering.space=embedding clustering.pca_components=10 \ + clustering.method=kmeans clustering.n_clusters=12 +``` ## Documentation -- [Getting started: typical usage on your own cohort](docs/getting_started.md) -- [Configuration guide: groups, presets, precedence, every knob by concern](docs/configuration_guide.md) -- [Dataset preparation](docs/dataset_preparation.md): manifest, inputs, labels, patient column, fold files -- [Training and inference workflows](docs/workflows_train_infer.md) -- [Nested cross-validation and regimes](docs/nested_cv.md) -- [Niche discovery and cross-fold replication](docs/niche_discovery.md) -- [Evaluation statistics](docs/evaluation_stats.md) -- [Classical spatial statistics](docs/spatial_statistics.md) -- [Inference utility suite](docs/inference_utility_suite.md) -- [Interpretability report suite](docs/interpretability_suite.md) and [extension guide](docs/interpretability_extension_guide.md) -- [Hyperparameter reference](docs/hyperparameter_reference.md) -- [Capabilities and modes](docs/capabilities_and_modes.md) -- [Model component contracts](docs/model_component_contracts.md) -- [Method correspondence with the preprint](docs/method_correspondence.md) -- [Repository map](docs/repository_map.md) +- [Data preparation](docs/data_preparation.md): cell tables, manifest, units, labels, folds, graphs +- [Training and evaluation](docs/training_and_evaluation.md): the model, pretraining, regimes, tasks, nested cross-validation, small cohorts, prediction +- [Niche analysis](docs/niche_analysis.md): per-cell outputs, niches, descriptors, survival, replication, model-free statistics +- [Configuration](docs/configuration.md): command-line basics, groups and every key ## Citation +If you use GRASS-MIL, please cite: + +> Rabuzin L, Yates J, Xu M, Boeva V. *GRASS-MIL: graph-based representation +> and discovery of phenotype-associated spatial structures with multiple +> instance learning.* bioRxiv (2026). +> doi: [10.64898/2026.10.08.757722](https://www.biorxiv.org/content/10.64898/2026.10.08.757722v1) + ```bibtex @article{rabuzin2026grassmil, title = {GRASS-MIL: graph-based representation and discovery of phenotype-associated spatial structures with multiple instance learning}, @@ -204,8 +187,6 @@ Every key is documented in [`docs/hyperparameter_reference.md`](docs/hyperparame } ``` -A machine-readable citation is in [`CITATION.cff`](CITATION.cff). - ## License and contact MIT license. Questions and issues: open a GitHub issue, or contact the Boeva diff --git a/docs/capabilities_and_modes.md b/docs/capabilities_and_modes.md deleted file mode 100644 index a890ac2..0000000 --- a/docs/capabilities_and_modes.md +++ /dev/null @@ -1,208 +0,0 @@ -# Capabilities And Modes - -This document catalogs operational modes and non-default capabilities in `grass-mil`. - -## 1) Training Regimes - -| Regime | Task | Model | Primary Use | -|---|---|---|---| -| Supervised mean | `finetune_mean` | `supervised_module` | Stable baseline graph-level classification/regression/survival | -| Supervised MIL | `finetune_mil` | `supervised_module` | Per-class attention aggregation; region cross-entropy is the sole objective | -| Cox survival MIL | `finetune_survival` | `supervised_module` | Scalar log-hazard per bag, scored by c-index | -| SSL pretrain | `pretrain_bgrl` | `bgrl_module` | Representation learning before supervised fine-tuning | -| GRASS-MIL pretrain (A3) | `regime=pretrain_a3` | `bgrl_a3` | The preprint's pretraining recipe | -| GRASS-MIL fine-tuning | `regime=mil` (`mil_frozen`, `linear_probehead`, `linear_ftall`, `linear_ftall_reg`) | `grass_mil` | One region = one bag, scaled learning rates, probe pre-fit, region-level selection | - -## 1b) Analysis Entry Points - -| CLI | Purpose | Doc | -|---|---|---| -| `grass-mil-probe` | logistic probe on the frozen encoder, folded into a linear head | `nested_cv.md` | -| `grass-mil-nested-cv` | nested patient-grouped cross-validation with out-of-fold predictions | `nested_cv.md` | -| `grass-mil-extract` | per-cell cache (embedding, instance logits, attention) of trained models | `niche_discovery.md` | -| `grass-mil-niches` | niche discovery and attention-weighted attribution on a model's cells | `niche_discovery.md` | -| `grass-mil-metaclusters` | cross-fold niche replication, transfer, shared basis | `niche_discovery.md` | -| `grass-mil-scores` | AUROC intervals, DeLong, operating points | `evaluation_stats.md` | -| `grass-mil-spatial-stats` | neighbourhood enrichment, cellular neighbourhoods, contacts, cross-K | `spatial_statistics.md` | -| `grass-mil-predict` | predictions, region/patient aggregation (mean or merged bag), embeddings, instance tables | `inference_utility_suite.md` | -| `grass-mil-report` | HTML/PDF interpretability report on instance tables | `interpretability_suite.md` | - -## 2) Aggregation And MIL Feature Modes - -| Mode | Config | Behavior | -|---|---|---| -| Mean bag aggregation | `task.aggregation=mean` | Averages instance logits per bag | -| MIL attention aggregation | `task.aggregation=mil_attention` | Learns attention weights over instance embeddings | -| Region accumulation | `task.region_accumulation.enabled=true` | Manual optimizer steps over region hyperbatches | -| One region per bag | `data.sampler.runtime.unit_as_bag=true` (`data/sampler=ego_graph_mil`) | All sampled ego-graphs of a region in one batch: one attention softmax, one loss | -| Patient aggregation | `aggregation.bag_scope=patient` with `mode=mean_probability` or `attention_weighted` | Mean of region probabilities, or one merged bag per patient | - -## 3) Backbone/Encoder Modes - -Supported `model.encoder.conv_type` values: - -- `gin` -- `gcn` -- `gat` -- `graphsage` -- `gine` - -Notable behavior: - -1. `gcn` and `gine` support edge-aware message passing. -2. `gine` requires edge feature dimensionality (`model.encoder.edge_attr_dim`) and runtime `edge_attr`. -3. `gat` requires `hidden_dim` divisible by `gat_heads`. -4. JK modes: `last`, `concat`, `max`, `sum`. -5. Pooling modes: `sum`, `mean`, `max`. Learned pooling was removed: instance - selection is the MIL head's job, pooling across ego-graphs. - -## 4) Data Sampling Modes - -Sampler strategies (`data.sampler.name`): - -- `identity`: standard graph batching. -- `shadow_native`: native PyG ShaDow sampler. -- `shadow_custom`: custom ShaDow implementation supporting per-subgraph transforms. - -Runtime controls (`data.sampler.runtime.*`): - -- depth and neighbor count (`num_neighbors=-1` = unlimited) -- subgraph batch size -- replacement and shuffling controls -- proportional root sampling by categorical property -- class-weight mode (`inverse`, `sqrt_inverse`, `proportional`) -- roots per unit per epoch (`subsample_count` / `subsample_fraction`), fixed per-unit seeds -- one batch per unit (`unit_as_bag`), roots from the union of units (`merge_units`) -- separate validation and test samplers (`data.val_sampler`, `data.test_sampler`) - -## 5) Loss/Task Objective Modes - -Supported `task.target_type`: - -- `binary` -- `categorical` (per-class attention; the GRASS-MIL default for binary tasks) -- `regression` -- `survival` - -Supported `task.loss` values (via model loss presets): - -- `categorical_bce` -- `categorical_ce` -- `regression_mse` -- `regression_huber` -- `survival_coxsgd` - -## 6) Runtime Device/Distribution Modes - -Trainer presets: - -- `trainer=cpu` -- `trainer=gpu` -- `trainer=mps` -- `trainer=ddp` -- `trainer=ddp_sim` - -Additional runtime toggles can be overridden directly via Hydra: - -- precision/mixed precision -- deterministic mode -- max/min epochs -- validation frequency - -## 7) Logging/Experiment Tracking Modes - -Logger presets: - -- `logger=csv` -- `logger=tensorboard` -- `logger=wandb` -- `logger=mlflow` -- `logger=comet` -- `logger=neptune` -- `logger=aim` -- `logger=many_loggers` (composition) - -Callbacks can be selected via `callbacks=...` group or disabled with `callbacks=none`. - -## 8) Debug/Development Modes - -Debug presets: - -- `debug=default` -- `debug=fdr` -- `debug=limit` -- `debug=overfit` -- `debug=profiler` - -Common usage: - -```bash -grass-mil-train debug=fdr -grass-mil-train debug=limit -``` - -## 9) Checkpoint Initialization And Resume Modes - -| Mode | Config | Behavior | -|---|---|---| -| Resume training | `ckpt_path=/abs/last.ckpt` | Resume optimizer/trainer state through Lightning fit | -| Initialize weights from checkpoint | `model.init_from_ckpt=/abs/ckpt` | Load state dict before training | -| BGRL-to-supervised key remap | `model.encoder_init_map=auto_bgrl_or_identity` (`spacegm_bgrl` for legacy checkpoints) | Remaps known encoder key prefixes | -| Probe head init | `task.head_init_path=` | Linear head from `grass-mil-probe`; `freeze_head` keeps it fixed | -| Strict load | `model.init_strict=true` | Fail on missing/unexpected keys | -| Freeze encoder | `model.freeze_encoder=true` | Train head/attention while encoder weights stay fixed | - -## 10) Inference Modes - -1. CLI evaluation mode: `src/grass_mil/eval.py` with required `ckpt_path`. -2. Advanced prediction mode: `Trainer.predict(...)` using `SupervisedModule.predict_step`. -3. Inference utility CLI mode: `src/grass_mil/inference/predict.py` for prediction export, metrics, and embeddings. - -Predict outputs include: - -- `bag_ids` -- `bag_logits` -- `row_region_ids` -- `row_sample_ids` -- optional `bag_targets` -- optional `bag_attention` -- optional `instance_*` fields -- optional embedding fields (`embedding_bag_ids`, `graph_embeddings`, `embedding_bag_counts`) -- optional node embedding fields (`node_embeddings`, `node_bag_ids`) - -Metrics available through the inference utility suite: - -- categorical/binary: accuracy, precision, recall, F1, ROC-AUC -- regression: R2, MAE, RMSE -- survival: concordance index (c-index) - -Validation (during `src/grass_mil/train.py`) includes: - -- step-level `val/loss` logging -- epoch-end region-level aggregation of buffered validation outputs -- task-specific epoch metrics: - - `val/acc`, `val/loss_region`, `val/auroc`, `val/bal_acc` for binary/categorical (region level) - - `val/mae`, `val/rmse`, `val/r2` for regression - - `val/c_index` for survival -- optional dedicated validation sampler via `data.val_sampler.*` - -## 11) Notebook Validation Modes - -Notebooks support interactive smoke validation of: - -- ingesting a public AnnData cohort and building cellular graphs - (`vignettes/01_anndata_to_spatial_graphs.ipynb`) -- self-supervised pretraining and spatial-niche discovery - (`vignettes/02_pretraining_and_niches.ipynb`) -- nested cross-validation, niche attribution, cross-fold metaclusters and scores - (`vignettes/03_nested_cv_niches_and_metaclusters.ipynb`) - -## 12) CI And Quality Modes - -Quality and validation tooling: - -- `pre-commit run -a` -- `pytest` -- GitHub Actions workflows for tests and code quality - -These operational modes are versioned in `.github/workflows/*.yaml` and `.pre-commit-config.yaml`. diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..5775c47 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,168 @@ +# Configuration + +Every command is a [Hydra](https://hydra.cc) application whose configuration +ships with the package (`src/grass_mil/configs`); your cohort file adds to it. + +## Command-line basics + +| Syntax | Effect | +|---|---| +| `key=value` | change a setting, e.g. `trainer.max_epochs=40`; lists as `[a,b]` | +| `group=option` | choose an option of a group, e.g. `regime=attention_only`, `trainer=gpu`, `task=survival` | +| `--config-dir configs` | also read configs from your `configs/` folder, which holds `experiment/my_cohort.yaml` | +| `hydra.run.dir=runs/` | the folder the command writes to (default `logs//runs/_