diff --git a/.cursor/rules/model_component_contracts.mdc b/.cursor/rules/model_component_contracts.mdc deleted file mode 100644 index 65a01a3..0000000 --- a/.cursor/rules/model_component_contracts.mdc +++ /dev/null @@ -1,62 +0,0 @@ ---- -description: Enforce clean-slate contracts for graph model components, losses, and samplers -alwaysApply: false ---- - -# Model Component Contracts - -## Data-Layer Agnostic Rule - -- Treat each dataset output as a generic graph unit (`torch_geometric.data.Data`). -- Do not assume outputs are patches. -- Do not assume outputs are full graphs. -- Samplers must consume whatever graph-unit granularity the data layer emits. - -## Required Graph Fields - -- `x` must be float tensor of shape `[num_nodes, num_features]`. -- `edge_index` must be `torch.long` tensor of shape `[2, num_edges]`. - -Optional fields: - -- `edge_attr`, `batch`, `pos`, `graph_y`, `graph_w`, `categorical_codes`. - -## Encoder Rules - -- Prefer native PyG operators over custom message passing. -- Allow only thin adapters around native PyG layers. -- Keep jump-knowledge and pooling behavior explicit and configurable. - -## Attention Rules - -- MIL attention heads must be optional and config-driven. -- Attention modules return logits; normalization is caller-controlled. - -## SSL Rules - -- SSL components must be optional and config-driven. -- Disabled SSL must instantiate no SSL modules. - -## Loss Rules - -- Support categorical, regression, and Cox survival objectives. -- Validate shapes and dtypes at boundaries. -- Avoid hidden broadcasting that can silently mis-train. - -## Sampler Rules - -- Provide identity and neighborhood strategies. -- Prefer native PyG neighborhood sampler when contracts are satisfied. -- Use custom sampler only when required behavior is unavailable in native path. - -## State and Side-Effect Rules - -- No mutable global runtime state in model and sampler modules. -- No script-specific hardcoded project logic. -- Constructors must be side-effect free. - -## Config-First Constants Rule - -- Use named configuration values for hyperparameters and architecture constants. -- Avoid undocumented inline numeric defaults in implementation code. -- Allow small structural literals only when they are shape-level invariants and self-evident. diff --git a/.env.example b/.env.example deleted file mode 100644 index a790e32..0000000 --- a/.env.example +++ /dev/null @@ -1,6 +0,0 @@ -# example of file for storing private and user specific environment variables, like keys or system paths -# rename it to ".env" (excluded from version control by default) -# .env is loaded by train.py automatically -# hydra allows you to reference variables in .yaml configs with special syntax: ${oc.env:MY_VAR} - -MY_VAR="/home/user/my/system/path" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d48477e..dda0175 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,7 +57,7 @@ jobs: -f "https://data.pyg.org/whl/torch-${TORCH_VERSION}+cpu.html" - name: Install the package - run: pip install ".[interpretability,io]" pytest + run: pip install ".[interpretability,io,stats,plots]" pytest - name: Show key package versions run: | @@ -113,7 +113,7 @@ jobs: - name: Run tests and collect coverage run: | - pip install ".[interpretability,io]" pytest "pytest-cov[toml]" + pip install ".[interpretability,io,stats,plots]" pytest "pytest-cov[toml]" pytest --cov grass_mil - name: Upload coverage to Codecov diff --git a/.gitignore b/.gitignore index 58302a1..69d0e65 100644 --- a/.gitignore +++ b/.gitignore @@ -145,6 +145,8 @@ dmypy.json # Lightning-Hydra-Template configs/local/default.yaml +src/grass_mil/configs/local/*.yaml +!src/grass_mil/configs/local/.gitkeep /data/ /logs/ .env @@ -154,3 +156,6 @@ configs/local/default.yaml # Vignette scratch space (downloaded data, graphs, runs) vignettes/vignette_work/ + +# vignette datasets are downloaded at runtime (squidpy.datasets.mibitof) +vignettes/data/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3fa2e37 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ +# 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`). + +### 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 + +- Removed stale component configs, the dummy dataset, the embeddings shim and internal audit notes; added `CITATION.cff`; `hydra-core` 1.3.5+. diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..e12d63c --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,33 @@ +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 +license: MIT +repository-code: https://github.com/BoevaLab/grass-mil +authors: + - family-names: Rabuzin + given-names: Lovro + - family-names: Yates + given-names: Josephine + - family-names: Xu + given-names: Mina + - family-names: Boeva + given-names: Valentina +preferred-citation: + type: article + title: "GRASS-MIL: graph-based representation and discovery of phenotype-associated spatial structures with multiple instance learning" + authors: + - family-names: Rabuzin + given-names: Lovro + - family-names: Yates + given-names: Josephine + - family-names: Xu + given-names: Mina + - family-names: Boeva + given-names: Valentina + journal: bioRxiv + year: 2026 + doi: 10.64898/2026.10.08.757722 + url: https://www.biorxiv.org/content/10.64898/2026.10.08.757722v1 diff --git a/Makefile b/Makefile index df29c6f..801692b 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,7 @@ - help: ## Show help @grep -E '^[.a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' -clean: ## Clean autogenerated files +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 @@ -10,18 +9,11 @@ clean: ## Clean autogenerated files find . | grep -E ".ipynb_checkpoints" | xargs rm -rf rm -f .coverage -clean-logs: ## Clean logs +clean-logs: ## Remove run outputs under logs/ rm -rf logs/** -format: ## Run pre-commit hooks +format: ## Run pre-commit hooks (ruff lint + format) pre-commit run -a -sync: ## Merge changes from main branch to your current branch - git pull - git pull origin main - test: ## Run the test suite pytest - -train: ## Train the model - grass-mil-train diff --git a/README.md b/README.md index 2737e44..65c0f5e 100644 --- a/README.md +++ b/README.md @@ -1,198 +1,212 @@ -# grass-mil - -Geometric deep learning framework for spatial omics data, built around a Hydra-configured pipeline from raw cell tables to graph-level training and evaluation. - -## Project Purpose And Architecture - -`grass-mil` provides a configurable end-to-end workflow for: - -1. Loading spatial omics data (`csv`/`tsv`, `h5ad`, `SingleCellExperiment`/`rds`) -2. Building graph units (tiling/polygon patches + PyG graphs) -3. Running supervised training (mean aggregation or MIL attention) -4. Running self-supervised pretraining (BGRL) -5. Evaluating checkpoints with reproducible Hydra config composition - -Core architecture: - -- Data pipeline: `src/grass_mil/data/` + `configs/data/` -- Model/runtime pipeline: `src/grass_mil/models/` + `configs/model/` + `configs/task/` -- Interpretability pipeline: `src/grass_mil/interpretability/` + `configs/interpretability/` -- Entrypoints: `src/grass_mil/train.py`, `src/grass_mil/eval.py` -- Configuration system: `configs/` (Hydra groups) - -## Interpretability - -`grass-mil` includes a first-class interpretability suite: - -- parameterized dimensionality reduction (`pca`, `umap`, `tsne`) -- parameterized clustering (`kmeans`, `agglomerative`, `hdbscan`, `spectral`, `optics`, `dbscan`) -- core (composition-based) and tier-2 (spatial) analyses via a plugin registry -- report generation to interactive Plotly HTML and static PDF (Playwright) - -Quick run: - -```bash -grass-mil-report \ - data.instance_table=/absolute/path/to/instance_table.csv \ - data.spatial_table=/absolute/path/to/spatial_table.csv -``` - -## Start Here - -### 1) Install +# GRASS-MIL: graph-based representation and discovery of phenotype-associated spatial structures with multiple instance learning + +[![Preprint](https://img.shields.io/badge/bioRxiv-10.64898%2F2026.10.08.757722-b31b1b)](https://www.biorxiv.org/content/10.64898/2026.10.08.757722v1) +[![Tests](https://github.com/BoevaLab/grass-mil/actions/workflows/test.yml/badge.svg)](https://github.com/BoevaLab/grass-mil/actions/workflows/test.yml) +[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) +![Python](https://img.shields.io/badge/python-%E2%89%A53.11-blue) + +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 +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) + +## 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. + +## Install ```bash -git clone git@github.com:BoevaLab/grass-mil.git +git clone https://github.com/BoevaLab/grass-mil.git cd grass-mil -python -m venv .venv -source .venv/bin/activate +python -m venv .venv && source .venv/bin/activate +pip install -e ".[interpretability,io,stats,plots]" +``` -# Editable install for development; drop -e for a plain install. -pip install -e . +`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). -# Optional extras: interpretability plots, report rendering, spatial statistics, -# and the h5ad/SingleCellExperiment readers. -pip install -e ".[interpretability,report,spatial-stats,io]" -``` +Installing exposes the CLI entry points below; the Hydra config tree ships +inside the package, so every command works from any directory. -Installing exposes the CLI entrypoints (`grass-mil-train`, `grass-mil-eval`, -`grass-mil-loocv`, `grass-mil-predict`, `grass-mil-report`). The Hydra config -tree ships inside the package, so it resolves from any working directory. +## Quick start (public demo data, CPU, a few minutes) -Hydra's `paths.root_dir` defaults to the `PROJECT_ROOT` environment variable -when set (`rootutils` exports it automatically from the `.project-root` marker -in a source checkout) and otherwise falls back to the current working -directory. Set it explicitly to place `data/` and `logs/` elsewhere: +The vignettes run the whole chain on `squidpy.datasets.mibitof()` (three +MIBI-TOF images, ~3,300 cells): ```bash -export PROJECT_ROOT=/path/to/your/workspace +pip install squidpy jupyter +jupyter lab vignettes/ ``` -### 2) Prepare New Data - -Create a raw manifest at `data/raw/manifest.csv` (or override `data.raw_manifest_path`) with at least: +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. -- `sample_id` -- `input_path` -- `input_type` - -Optional: - -- `region_id` -- `polygons_path` - -For the full onboarding checklist and dataset schema details, see -[`docs/dataset_preparation.md`](docs/dataset_preparation.md). - -### 3) Run Default Supervised Pipeline - -```bash -grass-mil-train task=finetune_mean model=supervised_module data=spatial_omics -``` +## Typical usage on your own cohort -### 4) Evaluate A Checkpoint +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: ```bash -grass-mil-eval \ - ckpt_path=/absolute/path/to/checkpoint.ckpt \ - task=finetune_mean \ - model=supervised_module \ - data=spatial_omics +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 ``` -## Documentation Index +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). -- Worked examples on a public dataset: [`vignettes/`](vignettes/README.md) -- Full tracked-file map: [`docs/repository_map.md`](docs/repository_map.md) -- Dataset onboarding and preparation checklist: [`docs/dataset_preparation.md`](docs/dataset_preparation.md) -- End-to-end workflows (new data, SSL pretrain->finetune, model adjustments, inference): [`docs/workflows_train_infer.md`](docs/workflows_train_infer.md) -- Inference utility suite (prediction artifacts, metrics, embeddings): [`docs/inference_utility_suite.md`](docs/inference_utility_suite.md) -- Interpretability suite architecture and usage: [`docs/interpretability_suite.md`](docs/interpretability_suite.md) -- Interpretability extension guide (plugins/new analyses): [`docs/interpretability_extension_guide.md`](docs/interpretability_extension_guide.md) -- Exhaustive hyperparameter reference for repo configs: [`docs/hyperparameter_reference.md`](docs/hyperparameter_reference.md) -- Capability modes and operational options: [`docs/capabilities_and_modes.md`](docs/capabilities_and_modes.md) -- Existing model contracts: [`docs/model_component_contracts.md`](docs/model_component_contracts.md) +## Workflows -## Supported Training/Inference Modes +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). -| Mode | Task Config | Model Config | Entrypoint | Primary Output | +| Step | Command | Preset / keys | Outputs | Doc | |---|---|---|---|---| -| Supervised mean aggregation | `task=finetune_mean` | `model=supervised_module` | `src/grass_mil/train.py` | Region/sample-level logits via mean bag aggregation | -| Supervised MIL attention | `task=finetune_mil` | `model=supervised_module` | `src/grass_mil/train.py` | Per-class attention-weighted bag logits | -| Cox survival with MIL | `task=finetune_survival` | `model=supervised_module` | `src/grass_mil/train.py` | Scalar log-hazard per bag, scored by c-index | -| SSL pretraining (BGRL) | `task=pretrain_bgrl` | `model=bgrl_module` | `src/grass_mil/train.py` | BGRL-pretrained encoder checkpoint | -| Checkpoint evaluation | any compatible task/model | matching training stack | `src/grass_mil/eval.py` | Test metrics from selected checkpoint | -| Advanced prediction (Python API) | compatible with `SupervisedModule.predict_step` | `model=supervised_module` | `Trainer.predict(...)` | `bag_ids`, `bag_logits`, `row_region_ids`, `row_sample_ids`, optional `bag_targets`, optional `bag_attention`, optional `instance_*`, optional `embedding_*` | - -## Required Inputs (Manifest Summary) - -Default data config expects: - -- Manifest path: `${paths.data_dir}/raw/manifest.csv` -- Processed output dir: `${paths.data_dir}/processed/spatial_omics` - -Manifest columns (defaults from `configs/data/spatial_omics.yaml`): - -| Column | Required | Meaning | -|---|---|---| -| `sample_id` | yes | Logical sample identifier | -| `input_path` | yes | Path to raw input table/file | -| `input_type` | yes | One of `csv`, `tsv`, `h5ad`, `sce`, `rds` | -| `region_id` | no | Region/group identifier for bagging/label scopes | -| `polygons_path` | no | Polygon definitions for patch extraction/filtering | - -See full input contracts and file examples in [`docs/workflows_train_infer.md`](docs/workflows_train_infer.md). - -## Common Command Patterns (Hydra Overrides) - -### Switch aggregation regime - -```bash -grass-mil-train task=finetune_mean model=supervised_module -grass-mil-train task=finetune_mil model=supervised_module -``` - -### Run SSL pretraining +| 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 ```bash -grass-mil-train task=pretrain_bgrl model=bgrl_module -``` - -### Change backbone +# 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=[] ... -```bash -grass-mil-train task=finetune_mil model=supervised_module \ - model.encoder.conv_type=gine \ - model.encoder.use_edge_attr=true \ - model.encoder.edge_attr_dim=2 -``` +# 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 ... -### Force data re-precompute +# 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 ... -```bash -grass-mil-train data.force_precompute=true +# 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 ``` -### Resume training - -```bash -grass-mil-train ckpt_path=/absolute/path/to/last.ckpt +Every key is documented in [`docs/hyperparameter_reference.md`](docs/hyperparameter_reference.md). + +## 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) + +## Citation + +```bibtex +@article{rabuzin2026grassmil, + title = {GRASS-MIL: graph-based representation and discovery of phenotype-associated spatial structures with multiple instance learning}, + author = {Rabuzin, Lovro and Yates, Josephine and Xu, Mina and Boeva, Valentina}, + journal = {bioRxiv}, + year = {2026}, + doi = {10.64898/2026.10.08.757722}, + url = {https://www.biorxiv.org/content/10.64898/2026.10.08.757722v1} +} ``` -## Where Outputs Go - -Hydra runtime outputs: - -- Run logs and artifacts: `${paths.log_dir}/${task_name}/runs//` -- Multiruns: `${paths.log_dir}/${task_name}/multiruns///` -- Checkpoints (default callback): `${paths.output_dir}/checkpoints/` - -Data precompute outputs: - -- Graph files: `${data.processed_dir}/graphs/.../*.pt` -- Index: `${data.processed_dir}/processed_index.json` -- Metadata: `${data.processed_dir}/metadata.json` +A machine-readable citation is in [`CITATION.cff`](CITATION.cff). -## Detailed documentation +## License and contact -All currently supported capabilities are documented in [`docs/workflows_train_infer.md`](docs/workflows_train_infer.md), [`docs/hyperparameter_reference.md`](docs/hyperparameter_reference.md), and [`docs/capabilities_and_modes.md`](docs/capabilities_and_modes.md). +MIT license. Questions and issues: open a GitHub issue, or contact the Boeva +lab (Department of Computer Science, ETH Zurich). diff --git a/docs/capabilities_and_modes.md b/docs/capabilities_and_modes.md index 7bb7ba9..a890ac2 100644 --- a/docs/capabilities_and_modes.md +++ b/docs/capabilities_and_modes.md @@ -10,6 +10,22 @@ This document catalogs operational modes and non-default capabilities in `grass- | 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 @@ -18,6 +34,8 @@ This document catalogs operational modes and non-default capabilities in `grass- | 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 @@ -48,17 +66,21 @@ Sampler strategies (`data.sampler.name`): Runtime controls (`data.sampler.runtime.*`): -- depth and neighbor count +- 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` @@ -125,7 +147,8 @@ grass-mil-train debug=limit |---|---|---| | 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` | Remaps known encoder key prefixes | +| 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 | @@ -158,7 +181,7 @@ 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` for binary/categorical + - `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.*` @@ -171,6 +194,8 @@ Notebooks support interactive smoke validation of: (`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 diff --git a/docs/configuration_guide.md b/docs/configuration_guide.md new file mode 100644 index 0000000..29e8825 --- /dev/null +++ b/docs/configuration_guide.md @@ -0,0 +1,223 @@ +# Configuration Guide + +Everything in `grass-mil` is configured through Hydra: a command composes a +tree of YAML files from `src/grass_mil/configs/`, and every leaf can be +overridden on the command line as `key.path=value`. This page explains how the +tree is composed, what the groups are, and how to change the things people +usually change. The exhaustive key-by-key tables are in the +[hyperparameter reference](hyperparameter_reference.md). + +## 1. How a command is composed + +Each CLI has a root config (`train.yaml`, `nested_cv.yaml`, `niches.yaml`, ...). +A root config lists *groups* in its `defaults`, in order; later entries override +earlier ones, and command-line overrides win over everything: + +``` +train.yaml defaults: data -> model -> optim -> scheduler -> task -> callbacks -> trainer -> paths + -> experiment (null) -> regime (null) -> debug (null) +``` + +A *preset* is a file in a group. `regime=mil` is the file +`configs/regime/mil.yaml`; it is "`@package _global_`", so it may set keys +anywhere in the tree and may swap other groups (`override /model: grass_mil`, +`override /task: finetune_mil`, `override /scheduler: cosine_mil_step`, +`override /callbacks: mil_selection`, `override /data/sampler: ego_graph_mil`). +That is how one word selects a whole training recipe. An `experiment=` file is +the same mechanism, meant for *your* settings (the cohort template in +`configs/experiment/example_cohort.yaml` is one). + +Precedence, lowest to highest: group file defaults -> `experiment=` -> `regime=` +-> command-line `key=value`. Two consequences: + +- a regime's values (e.g. `data.sampler.runtime.subsample_count=256`) beat the + same key in your experiment file; override it on the command line if needed; +- a key that does not exist yet needs `+key=value` (e.g. + `+trainer.fast_dev_run=true`); an existing key is set with `key=value`. + +Useful Hydra flags: `--cfg job` prints the fully composed config and exits; +`--info defaults` shows which files were merged; `-m key=a,b` runs a sweep. + +The composed config is also written to the run directory +(`/.hydra/config.yaml`) so every run is reproducible from its +folder. + +## 2. The groups and their presets + +| Group | Presets (default first) | What it decides | +|---|---|---| +| `data` | `spatial_omics` | inputs, graph building, labels, splits, tiling (section 3) | +| `data/sampler` | `shadow_custom`, `ego_graph_mil` (regimes), `ego_graph_pretrain` (pretraining), `identity`, `shadow_native` | how ego-graphs are drawn (section 4) | +| `model` | `supervised_module`, `grass_mil` (regimes), `bgrl_module`, `bgrl_a3` (pretraining) | the module: encoder + head + attention, or BGRL | +| `model/encoder` | `gin`, `gine_length` (GRASS-MIL), `gine`, `gcn`, `gat`, `graphsage` | encoder architecture (section 5) | +| `model/attention` | `gated_projected`, `gated` | attention net (section 6) | +| `task` | `finetune_mean`, `finetune_mil` (regimes), `finetune_survival`, `pretrain_bgrl`, `pretrain_bgrl_a3` | objective, bag definition, optimisation knobs, augmentation (section 7) | +| `regime` | `mil`, `mil_frozen`, `linear_probehead`, `linear_ftall`, `linear_ftall_reg`, `pretrain_a3` | the preprint recipes: fine-tuning (section 7) and pretraining (section 9) | +| `optim` | `adamw` | optimiser | +| `scheduler` | `cosine_epoch`, `cosine_mil_step` (regimes), `cosine_step` (pretraining) | learning-rate schedule | +| `callbacks` | `default`, `mil_selection` (regimes), `refit`, `pretrain_checkpoint`, `none` | checkpointing, early stopping (section 8) | +| `trainer` | `default` (cpu), `gpu`, `gpu_amp`, `mps`, `ddp`, `ddp_sim` | device, epochs, precision (section 11) | +| `experiment` | `example_cohort` (template), `grass_mil_pretrain_a3` (alias of `regime=pretrain_a3`) | your cohort file goes here; one experiment file per command | +| `logger` | `csv`, `tensorboard`, `wandb`, `mlflow`, ... | experiment tracking (off by default) | +| `debug` | `fdr`, `limit`, `overfit`, `profiler` | quick runs | + +The analysis CLIs have flat root configs rather than groups: `probe.yaml`, +`nested_cv.yaml`, `extract.yaml`, `scores.yaml`, `niches.yaml`, +`metaclusters.yaml`, `spatial_stats.yaml`, `inference/predict.yaml`. The ones +that build a model (`probe`, `nested_cv`, `extract`, `predict`, `eval`) +include the `train.yaml` tree, so `experiment=`, `regime=` and every `data.*` +/ `model.*` key work there too. + +## 3. Inputs, features and graph (`data.*`) + +| What | Key | Notes | +|---|---|---| +| where the regions are | `data.raw_manifest_path`, `data.processed_dir` | manifest columns: `sample_id,input_path,input_type[,region_id,polygons_path]`; relative input paths resolve against the manifest | +| what a unit is | `data.sample_unit=full|tile` (+ `data.tiling.{tile_size_um,stride_um,min_cells}`) | `full` = one graph per region | +| coordinate unit | `data.coord_scale_um` | multiplies the input coordinates; all distances below are in the resulting unit | +| cell-type column | `data.csv.categorical_label_columns=[cell_type]`, `data.categorical_features.include_labels=[cell_type]` | the vocabulary is learnt from the data and stored in `processed/metadata.json` | +| continuous node features | `data.csv.molecular_columns=[SIZE]` (`[]` for none; `null` = every non-coordinate numeric column) | h5ad: `data.h5ad.molecular_features`, `data.h5ad.molecular_layer` | +| feature reduction | `data.feature_reducer.name=identity|pca|...`, `fit_mode` | identity by default | +| edges | `data.graph_builder.name=delaunay|knn`, `kwargs.edge_features=[log_distance,neighbor]`, `kwargs.neighbor_cutoff_um` | `log_distance` = `clip((ln d - 2)/3, 0, 1)` (`log_lower`, `log_upper` adjustable); `distance` = raw length | +| labels | `data.graph_labels.{label_file,id_column,tasks,scope,patient_column}` | `scope` = `region|patch|sample`; `patient_column` groups, never a target | +| splits | `data.split.explicit.{train,val,test,id_kind}` (lists or files) or the persisted `data.split.{split_by,train_val_test_split}` | nested CV sets explicit splits per fold | +| which cells may be roots | `data.sampler.runtime.interior_seeds.*` | exclude the tissue rim | + +Which node-feature layout reaches the encoder: `x` = continuous columns +(after the reducer), `categorical_codes` = the cell-type code; the encoder sums +a learnt cell-type embedding and a linear projection of `x`. A cohort with cell +types only has `x` of width 0 and works unchanged. + +## 4. Ego-graphs (`data.sampler.runtime.*`) + +| What | Key (default in `ego_graph_mil`) | +|---|---| +| hops | `depth` (4) | +| neighbours kept per hop | `num_neighbors` (-1 = all) | +| radius cap | `ego_radius.enabled` (false), `radius_per_hop`, `radius_offset` | +| roots per region per epoch (bag size) | `subsample_count` (256; `subsample_fraction` as an alternative) | +| uniform or cell-type-weighted roots | `proportional_root_sampling` (false), `property_name`, `weight_mode` | +| one bag per region | `unit_as_bag` (true) | +| validation subset | `data.val_sampler.runtime.subsample_count` (1024) with `fixed_per_unit_seed` (0) | +| test / prediction | `data.test_sampler` (null = every cell once) | +| pretraining batches | `ego_graph_pretrain`: `merge_units=true`, `subgraph_batch_size=64` | + +Example, 3-hop ego-graphs capped at 150 units of distance, 128 roots per bag: +`data.sampler.runtime.depth=3 data.sampler.runtime.ego_radius.enabled=true data.sampler.runtime.ego_radius.radius_per_hop=50 data.sampler.runtime.subsample_count=128`. + +## 5. Encoder (`model.encoder.*`) + +Selected by `model/encoder=gine_length` (the GRASS-MIL default inside +`model=grass_mil` and `model=bgrl_a3`). + +| What | Key (default) | +|---|---| +| depth and width | `num_layers` (4), `hidden_dim` (256), `out_dim` (256) | +| convolution | `conv_type` (`gine`; `gin`, `gcn`, `gat`, `graphsage`), `train_eps` (true) | +| edge conditioning | `use_edge_attr` (true), `edge_feature_index` (0 = the `log_distance` column), `edge_attr_dim` (1) | +| normalisation, activation, dropout | `norm` (`layernorm`), `act` (`relu`), `dropout` (0 when fine-tuning, 0.1 in `bgrl_a3`) | +| readout | `pooling` (`max`), `jk` (`last`) | +| cell-type embedding | `categorical_embedding.{label,reserve_unassigned,unassigned_label}`; `input_init` (`xavier`) | + +`model.encoder.num_layers=3 model.encoder.hidden_dim=128 model.encoder.out_dim=128` +gives a smaller encoder; the head and attention widths follow `out_dim` +automatically. A pretrained checkpoint must have been trained with the same +encoder settings. + +## 6. Attention and instance head + +| What | Key (default) | +|---|---| +| attention net | `model/attention=gated_projected` (`gated` = no projection); `model.attention.projection_dim` (32), `hidden_dim` (8), `dropout` (true), `init` (`kaiming`) | +| channels | `model.attention.n_classes` (2, one per class; set with `model.graph_head.output_dim` for more classes) | +| instance head | `model.graph_head.num_layers` (2 = MLP; 1 = linear, required by the `linear_*` regimes), `hidden_dim`, `dropout` | +| pooling | always logit-space: `L_c = sum_i A[i,c] * l[i,c]` (`task.aggregation=mil_attention`); `task.aggregation=mean` averages instance logits instead | + +## 7. Training regimes and optimisation (`regime=`, `task.*`, `optim.*`) + +Pick a regime, then adjust: + +| What | Key (where the regimes set it) | +|---|---| +| learning rate, weight decay | `optim.lr` (1e-4), `optim.weight_decay` (1e-3; 1e-2 in `linear_ftall_reg`) | +| per-component rates | `task.optimization.encoder_lr_scale` (1 / 0.3 / 0 = frozen), `head_lr_scale`, `attention_lr_scale` (2), `head_weight_decay` | +| head pre-fit | `task.head_init_path` (`auto` in `linear_*`: fitted by nested CV; or a `grass-mil-probe` file), `task.freeze_head` | +| class weights | `task.class_weights` (`auto_train`, a list, or null) | +| augmentation while fine-tuning | `task.finetune_augmentation.{drop_edge_p,drop_feat_p}` (0.05 in `linear_ftall_reg`) | +| accumulation and clipping | `task.region_accumulation.hyperbatch_size` (12), `task.grad_clip_norm` (1.0) | +| schedule | `scheduler=cosine_mil_step` with `scheduler.T_max=auto` (whole run) and `task.scheduler_interval=step`; `scheduler.eta_min` | +| epochs | `trainer.max_epochs` (25), `trainer.min_epochs` (10, before early stopping may act) | +| what a bag is | `task.bag_key` (`region_id`; `patch_id` for tiles) | +| probe settings | `probe.{roots_per_unit,seed,C,class_weight,max_iter}` and `nested_cv.probe.*` | + +To define your own recipe, copy `configs/regime/mil.yaml` to +`configs/regime/my_regime.yaml`, edit, and run `regime=my_regime`. + +## 8. Selection and checkpoints (`callbacks=`) + +`mil_selection` (regimes): checkpoint on `val/auroc`, early stopping on +`val/loss_region` with `callbacks.early_stopping.patience` (8). `refit`: no +selection, last weights. `pretrain_checkpoint`: every +`callbacks.model_checkpoint.every_n_train_steps` (5000). Any Lightning +callback argument is reachable, e.g. +`callbacks.model_checkpoint.save_top_k=3`. + +Logged per epoch and written to `metrics_per_epoch.jsonl`: `val/loss_region`, +`val/auroc`, `val/bal_acc`, `val/acc`, `train/loss`. + +## 9. Pretraining (`regime=pretrain_a3`) + +| What | Key (default) | +|---|---| +| length | `task.total_steps` (200000), `task.warmup_steps` and `task.lr_warmup.warmup_steps` (1000) | +| optimiser | `optim.lr` (5e-5), `optim.weight_decay` (1e-5) | +| target network | `task.momentum` (0.99) -> `task.momentum_final` (1.0) | +| views | `task.augmentation.{mode,mu,p_lambda,feature_noise_std,feature_noise_columns}`; `augmentation=uniform` for plain edge/feature dropout | +| encoder dropout | `model.encoder.dropout` (0.1) | +| batches | `data.sampler.runtime.subgraph_batch_size` (64), `depth` (4) | +| data | the regime sets `data.split.train_val_test_split=[1,0,0]`: every region, no labels needed | + +Any `model.encoder.*` change here must be repeated when fine-tuning. + +## 10. Nested cross-validation (`nested_cv.*`) + +| What | Key (default) | +|---|---| +| folds and labels | `nested_cv.folds_file`, `nested_cv.{label_file,id_column,label_column,patient_column}` (taken from `data.graph_labels` when unset), `nested_cv.id_kind=region|sample|patch` | +| inner selection | `nested_cv.inner_k` (3), `inner_seed` (0), `selection_metric` (`val/auroc`), `selection_mode`, `epoch_reduce` (`median`), `min_refit_epochs` | +| which outer folds | `nested_cv.outer_folds=[2]` (null = all), `nested_cv.resume` (true) | +| probes | `nested_cv.probe.{enabled,cache_dir,roots_per_unit,...}` | +| patient score | `nested_cv.patient_aggregation` (`mean`) | + +## 11. Devices, precision, runtime + +`trainer=gpu` (one GPU), `trainer.devices=2 trainer.strategy=ddp`, +`trainer.precision=16-mixed` (the regimes' default; `32` on CPU), +`data.num_workers` (0 is usually fastest for ego-graph sampling, which creates a +loader per region), `seed=0` for reproducible runs, `paths.output_dir=...` to +choose where a run writes (default `logs//runs/`). + +## 12. Extraction, niches, metaclusters, statistics + +These read a cell cache, not the training config, so their keys are flat: + +| CLI | Keys people change | +|---|---| +| `grass-mil-extract` | `extract.nested_cv_summary` or `+extract.models.=`, `extract.unit_id`, `extract.regions`, `extract.batch_size`, `extract.output_dir` (model composed from the same `experiment=`/`regime=` as training) | +| `grass-mil-niches` | `regions=held_out|all|`, `space=gnn|attention`, `reduction.{method,n_components}`, `clustering.{method,n_clusters,params.min_cluster_size,params.min_samples,params.cluster_selection_method}`, `sample_size`, `l2_normalise`, `assign_k`, `present_threshold`, `n_boot`, `umap.*`, `figures` | +| `grass-mil-metaclusters` | the same clustering keys, `match.{distance,threshold,scan,min_folds}`, `transfer.{min_cells,present_threshold}`, `basis.{k,space_model,sample_size}`, `stage` | +| `grass-mil-scores` | `scores`, `fold_column`, `patient_column`, `patient_level`, `standardise`, `strata`, `operating_rules`, `n_boot` | +| `grass-mil-spatial-stats` | `group_map`, `reference`, `partners`, `infiltrating`, `analyses`, `nhood.{n_perms,backend}`, `cn.{window_k,n_cn}`, `crossk.radii`, `min_per_group` | + +Lists on the command line are written `key=[a,b]`; dictionaries +`key='{a: b, c: d}'`; `null` clears a value. + +## 13. Reading the defaults + +`src/grass_mil/configs/` is the source of truth; every file is commented. +Start from `regime/mil.yaml` (what a fine-tuning run composes), +`task/finetune_mil.yaml` (every MIL knob), `data/sampler/ego_graph_mil.yaml` +(ego-graphs), `model/encoder/gine_length.yaml` (encoder) and +`regime/pretrain_a3.yaml` (pretraining). The +[hyperparameter reference](hyperparameter_reference.md) lists every key with +its type, valid values and an example override. diff --git a/docs/dataset_preparation.md b/docs/dataset_preparation.md index 218176e..1809984 100644 --- a/docs/dataset_preparation.md +++ b/docs/dataset_preparation.md @@ -103,6 +103,35 @@ Behavior: Use a CSV file to attach supervision targets at `sample`, `region`, or `patch` scope. +```csv +REGION_ID,STAS,PATIENT +P001_core1,1,P001 +P001_core2,1,P001 +P002_core1,0,P002 +``` + +```bash +data.graph_labels.label_file=labels.csv data.graph_labels.id_column=REGION_ID \ +data.graph_labels.tasks=[STAS] data.graph_labels.patient_column=PATIENT +``` + +`patient_column` is never a target: it is attached to every graph as +`data.patient_id`, recorded in `processed_index.json`, and used by +`grass-mil-nested-cv` to keep a patient's regions in one fold and by +`grass-mil-predict` / `grass-mil-scores` for patient-level scores. + +### Fold files + +Cross-validation folds are a JSON list of lists of unit ids (regions by default): + +```json +[["P001_core1", "P001_core2", "P007_core1"], ["P002_core1", "..."], ["..."]] +``` + +Build them once, patient-grouped, and keep them with the data +(`grass_mil.data.components.splits.write_fold_json`); every regime is then +evaluated on identical folds. + ## 6) Run First Precompute/Training Pass Use a fresh processed directory when onboarding new data: diff --git a/docs/evaluation_stats.md b/docs/evaluation_stats.md new file mode 100644 index 0000000..1bdfefc --- /dev/null +++ b/docs/evaluation_stats.md @@ -0,0 +1,34 @@ +# Evaluation Statistics (`grass-mil-scores`) + +Held-out scores of a cross-validation come from different fold models with +different score scales. `grass-mil-scores` standardises them within fold before +pooling, reports both the per-fold mean and the pooled AUROC with bootstrap +intervals, compares scores with the DeLong test and tabulates operating points +at pre-specified rules. + +```bash +grass-mil-scores input=runs/nested_ftall_reg/oof_predictions.csv output_dir=runs/scores +grass-mil-scores input=patients.csv scores=[grass,clinical] strata=[subtype] patient_column=null +``` + +Input: a CSV with one row per unit, a `label_column` (default `label`), one or +more score columns (`scores`, default `[prob_class_1]`), optionally a fold column +(`fold_column`, default `outer`), a patient column (`patient_column`, default +`patient_id`; with `patient_level: true` the scores are averaged per patient +first) and strata columns. + +| output | content | +|---|---| +| `scores_summary.csv` | per score: pooled AUROC on within-fold standardised scores with a percentile bootstrap CI (`n_boot`, `seed`), per-fold AUROC mean and sd, raw pooled AUROC | +| `delong.csv` | every pair of scores: delta AUROC, z, p (Sun & Xu structural components; same labels) | +| `strata.csv` | AUROC with CI within each level of each stratum (first score) | +| `operating_points.csv` | per score and rule: sensitivity, specificity, PPV, NPV with bootstrap CIs, LR+, LR-, confusion counts; rules = flag the top `q` within each fold (`operating_rules`) plus Youden / rule-in / rule-out thresholds chosen on all data (flagged optimistic) | +| `ppv_npv_by_prevalence.csv` | PPV and NPV of the middle rule at other prevalences | +| `report_scores.md` | all of the above as tables | + +`standardise=z|rank|none` selects the within-fold transform. The functions are +importable from `grass_mil.evaluation` (`auroc`, `bootstrap_auroc_ci`, +`delong_test`, `z_within_fold`, `rank_within_fold`, `per_fold_auroc`, +`stratified_auroc`, `flag_top_within_fold`, `operating_point`, +`operating_point_bootstrap`, `likelihood_ratios`, `ppv_npv_at_prevalence`, +`threshold_points`). diff --git a/docs/getting_started.md b/docs/getting_started.md new file mode 100644 index 0000000..5435c63 --- /dev/null +++ b/docs/getting_started.md @@ -0,0 +1,189 @@ +# Getting Started: Typical Usage On Your Own Cohort + +This page is the end-to-end path from cell tables to niches. Each step is one +command; the data description is written once in a cohort config and reused. +For the meaning of every setting see the +[configuration guide](configuration_guide.md); for a runnable demo on public +data see the [vignettes](../vignettes/README.md). + +## 0. What you need + +| Input | Form | Example | +|---|---|---| +| one cell table per region | CSV (or h5ad / SingleCellExperiment): one row per cell with coordinates, a cell-type column and optional continuous features such as cell size | `raw/P001_core1.csv` with `cell_id,x,y,cell_type,SIZE` | +| manifest | CSV listing the regions: `sample_id,input_path,input_type,region_id` | `raw/manifest.csv` | +| labels | CSV with a region id, the target and a patient column | `raw/labels.csv` with `REGION_ID,STAS,PATIENT` | +| folds | JSON list of lists of region ids, patient-grouped | `raw/folds.json`: `[["P001_core1","P001_core2",...], [...], ...]` | + +Coordinates can stay in pixels: set `coord_scale_um` to the factor that brings +them to the unit you want to express distances in, and give `neighbor_cutoff_um` +in that unit. Regions are whole cores, ROIs or slides (`sample_unit: full`); a +cohort of a few large images is cut into tiles instead (see the end of this page). + +Write the folds once and keep them with the data: + +```python +from grass_mil.data.components.splits import write_fold_json +# folds = [[...region ids...], ...] built from your patient ids, e.g. with +# sklearn.model_selection.StratifiedGroupKFold over regions grouped by patient +write_fold_json(folds, "raw/folds.json") +``` + +Install (`pip install -e ".[interpretability,io,stats,plots]"`), then check the +entry points: `grass-mil-train --help`. + +## 1. Describe the cohort once + +Copy the template and fill in paths and column names: + +```bash +cp src/grass_mil/configs/experiment/example_cohort.yaml src/grass_mil/configs/experiment/my_cohort.yaml +``` + +(With an installed wheel, put the file in any directory `my_configs/experiment/` +and add `--config-dir my_configs` to every command.) From here on every command +starts with `experiment=my_cohort`. Confirm the composed configuration before +running anything: + +```bash +grass-mil-train experiment=my_cohort --cfg job | less +``` + +## 2. Build the graphs + +The first run precomputes the Delaunay graphs of every region into +`processed_dir` (`processed_index.json` + one `.pt` per region) and reuses them +afterwards. A dry run does only that: + +```bash +grass-mil-train experiment=my_cohort +trainer.fast_dev_run=true +``` + +Check `processed/metadata.json`: number of regions, cell-type vocabulary, +continuous feature names. Re-run with `data.force_precompute=true` whenever an +input, a graph setting or `coord_scale_um` changes. + +## 3. Pretrain the encoder (label-free) + +```bash +grass-mil-train experiment=my_cohort regime=pretrain_a3 paths.output_dir=runs/pretrain_a3 trainer=gpu +``` + +Every region is used, no labels. Defaults: 200k steps, 64 ego-graphs per step, +4-hop ego-graphs, DropImportance + noise on `SIZE`. Shorten with +`task.total_steps=50000`; without a cell-size feature set +`task.augmentation.feature_noise_columns=[]`. Output: +`runs/pretrain_a3/checkpoints/last.ckpt` (plus one checkpoint every 5000 steps). + +## 4. Nested cross-validation + +```bash +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_ftall_reg trainer=gpu +``` + +Per outer fold this fits the probe head on the inner training regions, trains +three inner runs with region-level selection, refits on the whole development +set for the median best epoch and scores the held-out fold on every cell. +Expect about one minute per epoch for 300 regions of 3,000 cells on one GPU. +Resumable: re-run the same command after an interruption. + +Outputs in `runs/nested_ftall_reg/`: + +| file | content | +|---|---| +| `nested_cv_summary.json` | `auc_mean`, `auc_sd`, `auc_folds`, refit epochs, run directories | +| `oof_predictions.csv` | one held-out score per region (`outer, region_id, patient_id, label, prob_class_1`) | +| `oof_patient_predictions.csv` | mean of the region scores per patient | +| `outer_/refit/checkpoints/last.ckpt` | the fold models | + +Run the other regimes the same way (`regime=mil`, `linear_probehead`, +`linear_ftall`) into their own output directories; the probes are cached and +shared through `nested_cv.probe.cache_dir`. + +## 5. Evaluation statistics + +```bash +grass-mil-scores input=runs/nested_ftall_reg/oof_predictions.csv output_dir=runs/scores_ftall_reg +``` + +Patient-level (mean of regions) bootstrap AUROC interval, per-fold mean, +operating points at "flag the top q" rules; add a second table's scores with +`scores=[...]` to get DeLong comparisons. See +[evaluation_stats.md](evaluation_stats.md). + +## 6. Extract every cell through the fold models + +```bash +grass-mil-extract experiment=my_cohort regime=linear_ftall_reg \ + extract.nested_cv_summary=runs/nested_ftall_reg/nested_cv_summary.json \ + extract.output_dir=runs/cells trainer=gpu +``` + +Writes `runs/cells/fold/.npz` with the embedding, instance logits +and attention of every cell, plus the cell types, positions and graph. The +held-out regions of every fold model are recorded. + +## 7. Niches of one model, and replication across models + +```bash +grass-mil-niches cells_dir=runs/cells/fold2 output_dir=runs/niches_fold2 positive_name=STAS +grass-mil-metaclusters cells_dir=runs/cells output_dir=runs/meta positive_name=STAS \ + labels.label_file=/path/to/my_cohort/raw/labels.csv labels.label_column=STAS labels.patient_column=PATIENT +``` + +The first clusters the held-out cells of one fold model (PCA-10, L2, HDBSCAN, +15-NN assignment) and writes `clusters.csv`, `cells.csv`, `report.md` and the +figures. The second does it for every fold model, matches the clusters across +folds into metaclusters, tests their patient-level shares, transfers every +model to every region and fits a shared label-free basis. Both accept any +clustering backend (`clustering.method=kmeans clustering.n_clusters=12`). See +[niche_discovery.md](niche_discovery.md). + +## 8. Model-free spatial statistics + +```bash +grass-mil-spatial-stats cells_dir=runs/cells/fold0 output_dir=runs/spatial positive_name=STAS \ + labels.label_file=/path/to/my_cohort/raw/labels.csv labels.label_column=STAS labels.patient_column=PATIENT \ + reference=[tumor] partners=[myeloid,CD8,Treg] infiltrating=[myeloid,CD8] \ + group_map='{normal: tumor, ki67_cd8: CD8, cd8: CD8}' +``` + +Neighbourhood enrichment, cellular neighbourhoods, contact fractions and +cross-K per region, compared between classes at the patient level. See +[spatial_statistics.md](spatial_statistics.md). + +## Variants + +**One split instead of nested CV.** Give explicit id lists: + +```bash +grass-mil-train experiment=my_cohort regime=mil model.init_from_ckpt=runs/pretrain_a3/checkpoints/last.ckpt \ + data.split.explicit.train=raw/train.json data.split.explicit.val=raw/val.json data.split.explicit.test=raw/test.json +``` + +For a `linear_*` regime, first `grass-mil-probe` with the same splits and pass +`task.head_init_path=`. + +**Patient-level prediction from one model** (`mean_probability` = mean of the +patient's regions, `attention_weighted` = one merged bag): + +```bash +grass-mil-predict experiment=my_cohort regime=linear_ftall_reg ckpt_path=runs/nested_ftall_reg/outer_2/refit/checkpoints/last.ckpt \ + aggregation.bag_scope=patient aggregation.mode=mean_probability \ + aggregation.patient_map.label_file=/path/to/my_cohort/raw/labels.csv +``` + +**Tiles as units** (a few large images): `data.sample_unit=tile`, +`data.tiling.tile_size_um=...`, labels keyed by `patch_id` with +`data.graph_labels.scope=patch`, folds of patch ids, `nested_cv.id_kind=patch`. +Vignette 3 does exactly this. + +**h5ad inputs**: replace the `csv:` block by `h5ad: {coord_source: obsm, +coord_key: spatial, categorical_label_columns: [cell_type]}` and use +`input_type: h5ad` in the manifest; the marker matrix becomes the continuous +features (`molecular_features` selects a subset). + +**Cell type only** (no continuous feature): `data.csv.molecular_columns=[]` +and `task.augmentation.feature_noise_columns=[]` for pretraining. diff --git a/docs/hyperparameter_reference.md b/docs/hyperparameter_reference.md index 6206c29..5f6c4e5 100644 --- a/docs/hyperparameter_reference.md +++ b/docs/hyperparameter_reference.md @@ -5,9 +5,12 @@ This reference covers repo-defined configuration keys and behavior for: - `configs/train.yaml` - `configs/eval.yaml` - `configs/loocv.yaml` +- `configs/probe.yaml`, `configs/nested_cv.yaml`, `configs/extract.yaml`, `configs/scores.yaml`, `configs/niches.yaml`, `configs/metaclusters.yaml`, `configs/spatial_stats.yaml` (analysis CLIs, section 12b) +- `configs/regime/*.yaml` (training regime presets, section 12a) +- `configs/experiment/grass_mil_pretrain_a3.yaml` - `configs/data/spatial_omics.yaml` - `configs/task/*.yaml` -- `configs/model/**/*.yaml` +- `configs/model/**/*.yaml` (presets `grass_mil`, `bgrl_a3`, generic modules, `encoder/`, `attention/` groups) - `configs/augmentation/*.yaml` - `configs/data/sampler/*.yaml` - `configs/optim/*.yaml` @@ -116,6 +119,19 @@ Notes: `data.split.loocv.holdout_id` or `loocv.fold_index`. If both are set, `loocv.fold_index` takes priority and a warning is emitted. +### Explicit splits (`data.split.explicit.*`) + +| Key | Default | Type | Valid Values | Active When | Example Override | +|---|---|---|---|---|---| +| `data.split.explicit.train` | `null` | list/str | unit ids, or a `.json` / `.txt` file of ids | re-partition an existing precompute | `grass-mil-train data.split.explicit.train=train.json` | +| `data.split.explicit.val` | `null` | list/str | as above (may equal `test` for refits) | | `data.split.explicit.val=[r1,r2]` | +| `data.split.explicit.test` | `null` | list/str | as above | | `data.split.explicit.test=test.json` | +| `data.split.explicit.id_kind` | `region` | str | `region`, `sample`, `patch` | explicit split set | `data.split.explicit.id_kind=patch` | + +Explicit lists override the persisted splits without re-processing the graphs +(ids not listed are dropped); they need `feature_reducer.name=identity` or +`fit_mode=global`. `grass-mil-nested-cv` and `grass-mil-probe` set them per fold. + ## 2.4 Manifest Field Mapping | Key | Default | Type | Valid Values | Active When | Example Override | @@ -179,20 +195,30 @@ Notes: ## 2.10 Sampler Keys -| Key | Default | Type | Valid Values | Active When | Example Override | +`data.sampler` is a config group (`data/sampler=shadow_custom` by default, +`ego_graph_mil` for MIL fine-tuning, `ego_graph_pretrain` for BGRL). Single +keys are overridden as `data.sampler.runtime.=...`. + +| Key | Default (`shadow_custom`) | Type | Valid Values | Active When | Example Override | |---|---|---|---|---|---| | `data.sampler.name` | `shadow_custom` | str | `identity`, `shadow_native`, `shadow_custom` | dataloader build | `grass-mil-train data.sampler.name=identity` | | `data.sampler.kwargs` | `{}` | dict | strategy kwargs | strategy-specific | `grass-mil-train data.sampler.kwargs='{}'` | | `data.sampler.runtime.enabled` | `true` | bool | `true/false` | shadow strategies | `grass-mil-train data.sampler.runtime.enabled=false` | -| `data.sampler.runtime.depth` | `2` | int | `>=1` | shadow runtime | `grass-mil-train data.sampler.runtime.depth=3` | -| `data.sampler.runtime.num_neighbors` | `8` | int | `>=1` | shadow runtime | `grass-mil-train data.sampler.runtime.num_neighbors=16` | -| `data.sampler.runtime.subgraph_batch_size` | `32` | int | `>=1` | runtime subgraph batching | `grass-mil-train data.sampler.runtime.subgraph_batch_size=64` | +| `data.sampler.runtime.depth` | `2` (`4` in `ego_graph_*`) | int | `>=1` | ego-graph hops | `grass-mil-train data.sampler.runtime.depth=3` | +| `data.sampler.runtime.num_neighbors` | `8` (`-1` in `ego_graph_*`) | int | `>=1`, or `-1` = every neighbour at every hop | shadow runtime | `grass-mil-train data.sampler.runtime.num_neighbors=-1` | +| `data.sampler.runtime.subgraph_batch_size` | `32` | int | `>=1` | ego-graphs per chunk (upper bound when `unit_as_bag`) | `grass-mil-train data.sampler.runtime.subgraph_batch_size=64` | | `data.sampler.runtime.replace` | `false` | bool | `true/false` | root sampling | `grass-mil-train data.sampler.runtime.replace=true` | | `data.sampler.runtime.shuffle_subgraphs` | `true` | bool | `true/false` | training shuffle behavior | `grass-mil-train data.sampler.runtime.shuffle_subgraphs=false` | -| `data.sampler.runtime.proportional_root_sampling` | `true` | bool | `true/false` | weighted root sampling | `grass-mil-train data.sampler.runtime.proportional_root_sampling=false` | +| `data.sampler.runtime.proportional_root_sampling` | `true` (`false` in `ego_graph_*`) | bool | `true/false` | cell-type weighted roots | `grass-mil-train data.sampler.runtime.proportional_root_sampling=false` | | `data.sampler.runtime.property_name` | `cell_type` | str | categorical label present in `categorical_slices` | weighted root sampling | `grass-mil-train data.sampler.runtime.property_name=cell_type` | | `data.sampler.runtime.weight_mode` | `inverse` | str | `inverse`, `sqrt_inverse`, `proportional` | weighted root sampling | `grass-mil-train data.sampler.runtime.weight_mode=sqrt_inverse` | | `data.sampler.runtime.min_weight` | `1.0e-6` | float | `>0` | weighted root sampling | `grass-mil-train data.sampler.runtime.min_weight=1.0e-4` | +| `data.sampler.runtime.subsample_fraction` | `1.0` | float | `(0, 1]` | fraction of candidate roots per unit per epoch | `grass-mil-train data.sampler.runtime.subsample_fraction=0.5` | +| `data.sampler.runtime.subsample_count` | `null` (`256` in `ego_graph_mil`) | int/null | `>=1` or null | absolute roots per unit per epoch (uniform, without replacement; overrides the fraction) | `grass-mil-train data.sampler.runtime.subsample_count=512` | +| `data.sampler.runtime.subsample_seed` | `null` | int/null | | seed of the root subsampling generator | `grass-mil-train data.sampler.runtime.subsample_seed=0` | +| `data.sampler.runtime.fixed_per_unit_seed` | `null` | int/null | | identical root subset per unit on every pass (validation) | `grass-mil-train data.val_sampler.runtime.fixed_per_unit_seed=0` | +| `data.sampler.runtime.unit_as_bag` | `false` (`true` in `ego_graph_mil`) | bool | `true/false` | one batch per unit holding all its selected roots (one bag, one softmax, one loss) | `grass-mil-train data.sampler.runtime.unit_as_bag=true` | +| `data.sampler.runtime.merge_units` | `false` (`true` in `ego_graph_pretrain`) | bool | `true/false` | pool all units into one graph and draw roots from the union (BGRL) | `grass-mil-train data.sampler.runtime.merge_units=true` | | `data.sampler.runtime.interior_seeds.enabled` | `false` | bool | `true/false` | shadow runtime | `grass-mil-train data.sampler.runtime.interior_seeds.enabled=true` | | `data.sampler.runtime.interior_seeds.n_hops` | `null` | int/null | `>=1`, `null` uses `depth` | `interior_seeds.enabled=true` | `grass-mil-train data.sampler.runtime.interior_seeds.n_hops=3` | | `data.sampler.runtime.interior_seeds.seed_tolerance_multiplier` | `1.0` | float | `>=0` | `interior_seeds.enabled=true` | `grass-mil-train data.sampler.runtime.interior_seeds.seed_tolerance_multiplier=1.5` | @@ -202,6 +228,8 @@ Notes: | `data.sampler.runtime.ego_radius.radius_per_hop` | `75.0` | float | `>0`, datamodule units | `ego_radius.enabled=true` | `grass-mil-train data.sampler.runtime.ego_radius.radius_per_hop=30.0` | | `data.sampler.runtime.ego_radius.radius_offset` | `55.0` | float | `>=0`, datamodule units | `ego_radius.enabled=true` | `grass-mil-train data.sampler.runtime.ego_radius.radius_offset=22.0` | | `data.sampler.runtime.ego_radius.keep_root_component` | `true` | bool | `true/false` | `ego_radius.enabled=true` | `grass-mil-train data.sampler.runtime.ego_radius.keep_root_component=false` | +| `data.val_sampler` | `null` (fixed 1024-root subset in `regime=mil`) | dict/null | full sampler config | validation loader | see `configs/regime/mil.yaml` | +| `data.test_sampler` | `null` | dict/null | full sampler config; `null` derives an all-roots, unweighted, no-bagging variant of `sampler` | test / predict loaders | `grass-mil-train data.test_sampler.runtime.subsample_count=1024` | Two notes on these, both of which cause silent misconfiguration rather than errors: @@ -227,6 +255,7 @@ Two notes on these, both of which cause silent misconfiguration rather than erro | `data.graph_labels.id_column` | `id` | str | column in label file | graph labels enabled | `grass-mil-train data.graph_labels.id_column=region_id` | | `data.graph_labels.tasks` | `null` | list[str]/null | task columns or null for all | graph labels enabled | `grass-mil-train data.graph_labels.tasks="[outcome]"` | | `data.graph_labels.scope` | `region` | str | `region`, `patch`, `sample` | graph labels enabled | `grass-mil-train data.graph_labels.scope=sample` | +| `data.graph_labels.patient_column` | `null` | str/null | column of the label file | graph labels enabled | `grass-mil-train data.graph_labels.patient_column=PATIENT` (attached as `data.patient_id`, recorded in the index; never a target) | | `data.transforms` | `[]` | list[dict] | importable transform specs | transform pipeline | `grass-mil-train data.transforms='[{name: grass_mil.data.components.transforms.CompositionVector, kwargs: {label_attr: label_cell_type}}]'` | ## 2.12 Sampler Preset Files (`configs/data/sampler/*.yaml`) @@ -236,6 +265,8 @@ Two notes on these, both of which cause silent misconfiguration rather than erro | `configs/data/sampler/identity.yaml` | `name`, `kwargs` | `name=identity`, `kwargs={}` | | `configs/data/sampler/shadow_native.yaml` | `name`, `kwargs`, `runtime.*` | `name=shadow_native`, runtime defaults mirror `data.sampler.runtime.*` | | `configs/data/sampler/shadow_custom.yaml` | `name`, `kwargs`, `runtime.*` | `name=shadow_custom`, runtime defaults mirror `data.sampler.runtime.*` | +| `configs/data/sampler/ego_graph_mil.yaml` | `name`, `kwargs`, `runtime.*` | GRASS-MIL fine-tuning: depth 4, unlimited neighbours, uniform roots, `subsample_count=256`, `unit_as_bag=true` | +| `configs/data/sampler/ego_graph_pretrain.yaml` | `name`, `kwargs`, `runtime.*` | BGRL: depth 4, unlimited neighbours, 64 roots per step from the union of all units (`merge_units=true`) | ## 3) Task Config Group (`configs/task/*.yaml`) @@ -258,18 +289,35 @@ Two notes on these, both of which cause silent misconfiguration rather than erro ## 3.2 `configs/task/finetune_mil.yaml` -Includes all keys above plus MIL-specific controls: - -| Key | Default | Type | Valid Values | Active When | Example Override | -|---|---|---|---|---|---| -| `task.name` | `finetune_mil` | str | label | always | `grass-mil-train task.name=mil_exp` | -| `task.aggregation` | `mil_attention` | str | `mil_attention` | supervised module | `grass-mil-train task.aggregation=mil_attention` | -| `task.optimization.backbone_lr` | `null` | float/null | lr value or null | MIL optimizer param groups | `grass-mil-train task.optimization.backbone_lr=1e-3` | -| `task.optimization.attention_lr` | `null` | float/null | lr value or null | MIL optimizer param groups | `grass-mil-train task.optimization.attention_lr=4e-3` | -| `task.region_accumulation.enabled` | `false` | bool | `true/false` | MIL training only | `grass-mil-train task.region_accumulation.enabled=true` | -| `task.region_accumulation.mode` | `manual_region_buffer` | str | currently manual mode in code | MIL accumulation metadata | `grass-mil-train task.region_accumulation.mode=manual_region_buffer` | -| `task.region_accumulation.hyperbatch_size` | `8` | int | `>=1` when enabled | MIL accumulation | `grass-mil-train task.region_accumulation.hyperbatch_size=4` | -| `task.region_accumulation.flush_on_epoch_end` | `true` | bool | `true/false` | MIL accumulation | `grass-mil-train task.region_accumulation.flush_on_epoch_end=true` | +Includes the keys of 3.1 (with `aggregation=mil_attention`, `target_type=categorical`, +`loss=categorical_ce`, and per-class widths `model.graph_head.output_dim=2`, +`model.attention.n_classes=2`) plus the MIL controls below. The `regime=` +presets (section 12a) set them to the preprint values. + +| Key | Default | Type | Valid Values | Active When | Example Override | +|---|---|---|---|---|---| +| `task.bag_key` | `region_id` | str | `region_id`, `patch_id`, `sample_id` | what one bag is | `grass-mil-train task.bag_key=patch_id` | +| `task.class_weights` | `auto_train` | str/list/null | `auto_train` = inverse frequency on the training units (mean 1); a list; `null` | categorical loss | `grass-mil-train task.class_weights=[1.0,2.0]` | +| `task.optimization.encoder_lr_scale` | `1.0` | float | `>=0`; `0` freezes the encoder (no gradients, eval mode) | optimizer groups | `grass-mil-train task.optimization.encoder_lr_scale=0.3` | +| `task.optimization.head_lr_scale` | `1.0` | float | `>=0` | optimizer groups | `grass-mil-train task.optimization.head_lr_scale=0.01` | +| `task.optimization.attention_lr_scale` | `2.0` | float | `>=0` | optimizer groups | `grass-mil-train task.optimization.attention_lr_scale=1.0` | +| `task.optimization.head_weight_decay` | `null` | float/null | overrides `optim.weight_decay` for the head group | optimizer groups | `grass-mil-train task.optimization.head_weight_decay=1e-3` | +| `task.optimization.backbone_lr` / `attention_lr` | `null` | float/null | legacy absolute pair (both or neither); ignored when scales are used | | | +| `task.finetune_augmentation.drop_edge_p` | `0.0` | float | `[0,1]` | training batches only; edges dropped with both directions | `grass-mil-train task.finetune_augmentation.drop_edge_p=0.05` | +| `task.finetune_augmentation.drop_feat_p` | `0.0` | float | `[0,1]` | elementwise feature drop, writes the unassigned cell-type token | `grass-mil-train task.finetune_augmentation.drop_feat_p=0.05` | +| `task.finetune_augmentation.seed` | `0` | int | | augmentation generator | | +| `task.head_init_path` | `null` | str/null | probe head file (`graph_head.net.0.*`), or `auto` (resolved per split by `grass-mil-nested-cv`) | linear head (`model.graph_head.num_layers=1`) | `grass-mil-train task.head_init_path=probe_head.pt` | +| `task.freeze_head` | `false` | bool | | head excluded from the optimizer | `grass-mil-train task.freeze_head=true` | +| `task.grad_clip_norm` | `1.0` | float/null | `>0` or null | gradient-norm clipping in the region flush | `grass-mil-train task.grad_clip_norm=null` | +| `task.scheduler_interval` | `step` | str | `step`, `epoch` | with `scheduler=cosine_mil_step` (`T_max: auto`) the cosine spans every optimizer step of the run; skipped AMP steps do not advance it | `grass-mil-train task.scheduler_interval=epoch` | +| `task.region_accumulation.enabled` | `true` | bool | `true/false` | MIL training | `grass-mil-train task.region_accumulation.enabled=false` | +| `task.region_accumulation.hyperbatch_size` | `12` | int | `>=1` | regions per optimizer step | `grass-mil-train task.region_accumulation.hyperbatch_size=8` | +| `task.region_accumulation.flush_on_epoch_end` | `true` | bool | | | | +| `task.max_instances_per_bag` / `instance_sampling` | `0` / `all` | | as in 3.1 | | | + +Region-level metrics are logged per epoch (`val/loss_region`, `val/auroc`, +`val/bal_acc`, `val/acc`, `val/pred_pos_frac`; the same under `test/`) and written +to `metrics_per_epoch.jsonl`; `test_predictions.csv` holds one row per bag. ## 3.3 `configs/task/pretrain_bgrl.yaml` @@ -297,6 +345,26 @@ Includes all keys above plus MIL-specific controls: | `task.drop_feat_p1` | `0.2` | float | `[0,1]` | view-1 feature dropout | `grass-mil-train task.drop_feat_p1=0.1` | | `task.drop_feat_p2` | `0.2` | float | `[0,1]` | view-2 feature dropout | `grass-mil-train task.drop_feat_p2=0.3` | +`task.momentum_min` is the legacy name of `task.momentum_final`. + +## 3.4 `configs/task/pretrain_bgrl_a3.yaml` (GRASS-MIL pretraining) + +Selected by `regime=pretrain_a3` (with `model=bgrl_a3`, `callbacks=pretrain_checkpoint`, +`data/sampler=ego_graph_pretrain`); `experiment=grass_mil_pretrain_a3` is an alias. + +| Key | Default | Meaning | +|---|---|---| +| `optim.lr` / `optim.weight_decay` | `5e-5` / `1e-5` | AdamW | +| `task.lr_warmup.*` | enabled, `1000` steps, `start_factor=0.0` | linear warmup, then cosine | +| `scheduler.T_max` | `auto` | `= total_steps - warmup` so the LR reaches `eta_min=1e-7` at the end | +| `task.total_steps` | `200000` | optimizer steps (`trainer.max_steps` follows it) | +| `task.momentum` / `task.momentum_final` | `0.99` / `1.0` | target-encoder EMA, cosine, applied after every optimizer step | +| `task.momentum_warmup_steps` | `0` | no momentum warmup (`null` falls back to `task.warmup_steps`) | +| `task.augmentation` | `mode=importance`, `mu=0.25`, `p_lambda=0.45`, `feature_noise_std=0.2`, `feature_noise_columns=[SIZE]` | symmetric views: degree-importance drop with the unassigned token + Gaussian noise on the named continuous feature(s); set `[]` without a size feature | +| `model.encoder.dropout` | `0.1` (in `model=bgrl_a3`) | encoder dropout during pretraining only | +| `callbacks.model_checkpoint.every_n_train_steps` | `5000` | all step checkpoints kept, plus `last.ckpt` | +| `data.split.train_val_test_split` | `[1,0,0]` | every region used label-free | + ## 4) Model Config Group (`configs/model/**/*.yaml`) ## 4.1 `configs/model/supervised_module.yaml` @@ -320,16 +388,24 @@ Includes all keys above plus MIL-specific controls: | `model.encoder.use_edge_attr` | `false` | bool | `true/false` | `gcn`/`gine` meaningful | `grass-mil-train model.encoder.use_edge_attr=true` | | `model.encoder.edge_weight_index` | `0` | int | column index | `gcn` with edge attrs | `grass-mil-train model.encoder.edge_weight_index=1` | | `model.encoder.edge_attr_dim` | `null` | int/null | required for `gine` | `conv_type=gine` | `grass-mil-train model.encoder.edge_attr_dim=2` | +| `model.encoder.edge_feature_index` | `null` | int/null | column of `edge_attr` fed to the convolution (`gine_length`: `0`) | `conv_type=gine` | `grass-mil-train model.encoder.edge_feature_index=0` | +| `model.encoder.train_eps` | `true` | bool | learnable GIN/GINE epsilon | `gin`/`gine` | `grass-mil-train model.encoder.train_eps=false` | +| `model.encoder.input_init` | `xavier` | str | `xavier`, `default` | input embedding + linear init | `grass-mil-train model.encoder.input_init=default` | +| `model.encoder.categorical_embedding.label` | `cell_type` | str | categorical label name | summed cell-type embedding | | +| `model.encoder.categorical_embedding.num_embeddings` / `column_index` | `null` | int/null | resolved from the processed dataset | | | +| `model.encoder.categorical_embedding.reserve_unassigned` | `true` | bool | reserve a trailing row as the unassigned token | augmentations | | +| `model.encoder.categorical_embedding.unassigned_label` / `unassigned_index` | `null` | str/int/null | reuse an existing category (e.g. `other`) as the unassigned token | | `grass-mil-train model.encoder.categorical_embedding.unassigned_label=other` | | `model.attention.attention_type` | `gated_projected` | str | `gated`, `gated_projected` | `task.aggregation=mil_attention` | `grass-mil-train model.attention.attention_type=gated` | | `model.attention.input_dim` | `128` | int | `>=1` | MIL attention | `grass-mil-train model.attention.input_dim=256` | -| `model.attention.projection_dim` | `64` | int | `>=1` | projected attention | `grass-mil-train model.attention.projection_dim=128` | -| `model.attention.hidden_dim` | `16` | int | `>=1` | MIL attention | `grass-mil-train model.attention.hidden_dim=32` | -| `model.attention.dropout` | `false` | bool | `true/false` | MIL attention | `grass-mil-train model.attention.dropout=true` | -| `model.attention.n_classes` | `1` | int | `>=1` | MIL attention output channels | `grass-mil-train model.attention.n_classes=1` | +| `model.attention.projection_dim` | `32` | int | `>=1` | projected attention | `grass-mil-train model.attention.projection_dim=64` | +| `model.attention.hidden_dim` | `8` | int | `>=1` | MIL attention (the "attention space" width) | `grass-mil-train model.attention.hidden_dim=16` | +| `model.attention.dropout` | `true` | bool | `true/false` | dropout 0.25 in the attention net while training | `grass-mil-train model.attention.dropout=false` | +| `model.attention.init` | `kaiming` | str | `kaiming`, `default` | attention linears init | `grass-mil-train model.attention.init=default` | +| `model.attention.n_classes` | `1` (`2` with `task=finetune_mil`) | int | `>=1` | one attention channel per class | `grass-mil-train model.attention.n_classes=3` | | `model.graph_head.input_dim` | `128` | int | `>=1` | graph head | `grass-mil-train model.graph_head.input_dim=256` | | `model.graph_head.output_dim` | `1` | int | `>=1` | graph head | `grass-mil-train model.graph_head.output_dim=2` | | `model.graph_head.hidden_dim` | `128` | int | `>=1` | graph head | `grass-mil-train model.graph_head.hidden_dim=256` | -| `model.graph_head.num_layers` | `2` | int | `>=1` | graph head | `grass-mil-train model.graph_head.num_layers=3` | +| `model.graph_head.num_layers` | `2` | int | `>=1`; `1` = linear instance head (required by the probe regimes) | graph head | `grass-mil-train model.graph_head.num_layers=1` | | `model.graph_head.dropout` | `0.0` | float | `[0,1]` | graph head | `grass-mil-train model.graph_head.dropout=0.2` | | `model.ssl.method` | `bgrl` | str | `bgrl` | SSL factory | `grass-mil-train model.ssl.method=bgrl` | | `model.ssl.predictor.hidden_size` | `512` | int | `>=1` | SSL enabled | `grass-mil-train model.ssl.predictor.hidden_size=256` | @@ -339,7 +415,7 @@ Includes all keys above plus MIL-specific controls: | `model.task` | `${task}` | DictConfig | task config object | always | `grass-mil-train task=finetune_mil` | | `model.init_from_ckpt` | `null` | str/null | checkpoint path | optional init | `grass-mil-train model.init_from_ckpt=/abs/pretrain.ckpt` | | `model.init_strict` | `false` | bool | `true/false` | checkpoint init | `grass-mil-train model.init_strict=true` | -| `model.encoder_init_map` | `auto_bgrl_or_identity` | str | `identity`, `auto_bgrl_or_identity` | checkpoint init | `grass-mil-train model.encoder_init_map=identity` | +| `model.encoder_init_map` | `auto_bgrl_or_identity` | str | `identity`, `auto_bgrl_or_identity`, `spacegm_bgrl` (legacy SPACE-GM BGRL checkpoints) | checkpoint init | `grass-mil-train model.encoder_init_map=spacegm_bgrl` | | `model.freeze_encoder` | `false` | bool | `true/false` | checkpoint transfer/finetuning | `grass-mil-train model.freeze_encoder=true` | ## 4.2 `configs/model/bgrl_module.yaml` @@ -360,134 +436,67 @@ Same encoder block as supervised module plus SSL/pretrain init keys: | `model.init_strict` | `false` | bool | `true/false` | checkpoint init | `grass-mil-train model.init_strict=true` | | `model.encoder_init_map` | `auto_bgrl_or_identity` | str | `identity`, `auto_bgrl_or_identity` | checkpoint init | `grass-mil-train model.encoder_init_map=identity` | -## 4.3 Component Preset Files +## 4.3 Model Presets And Component Groups -### `configs/model/components.yaml` +### `configs/model/grass_mil.yaml` (GRASS-MIL supervised model) -| Key | Default | Type | Valid Values | Active When | Example Override | -|---|---|---|---|---|---| -| `model.flags.use_ssl` | `false` | bool | `true/false` | component notebook/testing | `grass-mil-train model.flags.use_ssl=true` | -| `model.attention.attention_type` | `gated_projected` | str | `gated`, `gated_projected` | `task.aggregation=mil_attention` | `grass-mil-train model.attention.attention_type=gated` | -| `model.encoder.*` | same fields as section 4.1 | mixed | see section 4.1 | component testing | `grass-mil-train model.encoder.conv_type=gcn` | -| `model.attention.*` | defaults per file | mixed | see attention files below | component testing | `grass-mil-train model.attention.hidden_dim=32` | -| `model.ssl.method` | `bgrl` | str | `bgrl` | component testing | `grass-mil-train model.ssl.method=bgrl` | -| `model.ssl.predictor.hidden_size` | `512` | int | `>=1` | component testing | `grass-mil-train model.ssl.predictor.hidden_size=256` | -| `model.graph_head.*` | see section 4.1 | mixed | MLP head args | component testing | `grass-mil-train model.graph_head.num_layers=3` | +`defaults: [encoder: gine_length, attention: gated_projected]`, MLP instance head +(`graph_head.num_layers=2`, hidden = `encoder.out_dim`), bag logit pooled in logit +space (`L_c = sum_i A[i,c] f_c(h_i)`). Use `model=grass_mil task=finetune_mil` or a +`regime=` preset. `model.graph_head.num_layers=1` gives the linear head of the +probe regimes. -Attention is **not** toggled by a model flag. `SupervisedModule` derives it from -the task: `use_attention = (task.aggregation == "mil_attention")`. Use -`task=finetune_mil` to enable it and `task=finetune_mean` to disable it. +### `configs/model/bgrl_a3.yaml` (GRASS-MIL pretraining model) -The component sub-configs (`model.encoder`, `model.attention`, -`model.graph_head`, `model.ssl.predictor`) are plain option dicts consumed by -`build_supervised_components`, not Hydra `_target_` instantiations. Selecting a -different attention implementation is therefore -`model.attention.attention_type=gated`, not a `_target_` override. +`defaults: [encoder: gine_length]`, `encoder.dropout=0.1`, predictor hidden 512. -`task=finetune_mil` sets the per-class widths itself -(`model.graph_head.output_dim=2`, `model.attention.n_classes=2`). This works -because `task` is merged after `model` in the root defaults list; see the -comment in `configs/train.yaml`. +### `configs/model/supervised_module.yaml`, `configs/model/bgrl_module.yaml` -### Encoder preset files (`configs/model/encoder/*.yaml`) +Generic modules: `defaults: [encoder: gin, attention: gated_projected]`. Switch +the encoder with `model/encoder=gine_length` and the attention with +`model/attention=gated`. -Covered files: - -- `configs/model/encoder/gin.yaml` -- `configs/model/encoder/gcn.yaml` -- `configs/model/encoder/gat.yaml` -- `configs/model/encoder/graphsage.yaml` -- `configs/model/encoder/gine.yaml` -- `configs/model/encoder/gine_length.yaml` +Attention is **not** toggled by a model flag. `SupervisedModule` derives it from +the task: `use_attention = (task.aggregation == "mil_attention")`. Use +`task=finetune_mil` to enable it and `task=finetune_mean` to disable it. The +component sub-configs (`model.encoder`, `model.attention`, `model.graph_head`, +`model.ssl.predictor`) are plain option dicts consumed by +`build_supervised_components`, not `_target_` instantiations: select another +attention implementation with `model.attention.attention_type=gated`. -All six files expose the same base keys: +`task=finetune_mil` sets the per-class widths itself +(`model.graph_head.output_dim=2`, `model.attention.n_classes=2`); `task` is merged +after `model` in the root defaults list (see `configs/train.yaml`). -- `_target_`, `input_dim`, `hidden_dim`, `out_dim`, `num_layers`, `dropout`, - `conv_type`, `norm`, `jk`, `act`, `pooling`, `gat_heads`, `use_edge_attr`, - `edge_weight_index`, `edge_attr_dim` +### Encoder group (`configs/model/encoder/*.yaml`) -Per-file defaults differ mainly in `conv_type`, `norm`, `use_edge_attr`, and -`edge_attr_dim`: +Files: `gin`, `gcn`, `gat`, `graphsage`, `gine`, `gine_length`. All expose the +keys of section 4.1; per-file defaults differ in `conv_type`, `norm`, +`use_edge_attr`, `edge_attr_dim`: -- `gin.yaml`: `conv_type=gin`, `norm=batchnorm`, `use_edge_attr=false`, `edge_attr_dim=null` -- `gcn.yaml`: `conv_type=gcn`, `norm=batchnorm`, `use_edge_attr=true`, `edge_attr_dim=null` -- `gat.yaml`: `conv_type=gat`, `norm=batchnorm`, `use_edge_attr=false`, `edge_attr_dim=null` -- `graphsage.yaml`: `conv_type=graphsage`, `norm=batchnorm`, `use_edge_attr=false`, `edge_attr_dim=null` +- `gin.yaml`: `conv_type=gin`, `norm=batchnorm`, `use_edge_attr=false` +- `gcn.yaml`: `conv_type=gcn`, `norm=batchnorm`, `use_edge_attr=true` +- `gat.yaml`: `conv_type=gat`, `norm=batchnorm` +- `graphsage.yaml`: `conv_type=graphsage`, `norm=batchnorm` - `gine.yaml`: `conv_type=gine`, `norm=batchnorm`, `use_edge_attr=true`, `edge_attr_dim=2` -- `gine_length.yaml`: `conv_type=gine`, `norm=layernorm`, `use_edge_attr=true`, `edge_attr_dim=1` - -`gine_length.yaml` is the production encoder and the only preset that sets two -further keys: - -- `edge_feature_index: 0` — narrows `edge_attr` to the scalar edge-length - column, so the convolution conditions on distance alone. There is no notion of - edge type. -- `categorical_embedding: {label: cell_type, num_embeddings: null, column_index: null}` - — adds the cell-type embedding, summed with the projected continuous features - (`h⁰ = E[t] + Wₓx`). The two `null`s are resolved at build time from the - processed dataset's label map and categorical slices; set them explicitly to - override. - -Because the embedding can carry the whole node representation, this preset is -the one that supports cell-type-only cohorts (`input_dim=0`). +- `gine_length.yaml` (GRASS-MIL): `conv_type=gine`, 4 x 256, `norm=layernorm`, + `pooling=max`, `edge_attr_dim=1`, `edge_feature_index=0` (the bounded log + edge length), `train_eps=true`, `input_init=xavier`, `categorical_embedding` + with a reserved unassigned row. Supports cell-type-only cohorts (`input_dim=0`). -### Attention preset files (`configs/model/attention/*.yaml`) - -Covered files: - -- `configs/model/attention/gated.yaml` -- `configs/model/attention/gated_projected.yaml` - -| File | Keys | Defaults | -|---|---|---| -| `gated.yaml` | `_target_`, `input_dim`, `hidden_dim`, `dropout`, `n_classes` | `AttnNetGated`, `128`, `64`, `false`, `1` | -| `gated_projected.yaml` | `_target_`, `input_dim`, `projection_dim`, `hidden_dim`, `dropout`, `n_classes` | `AttnNetGatedProjected`, `128`, `64`, `16`, `false`, `1` | - -### Head preset files (`configs/model/heads/*.yaml`) - -Covered files: - -- `configs/model/heads/graph.yaml` - -`graph.yaml` exposes: - -- `_target_`, `input_dim`, `output_dim`, `hidden_dim`, `num_layers`, `dropout` - -Defaults: - -- `input_dim=128`, `output_dim=1`, `hidden_dim=128`, `num_layers=2`, `dropout=0.0` - -There is no node head preset. `NodePredictionHead` existed only to serve the -instance-pull auxiliary loss, which is not part of the published objective and -has been removed along with it; region cross-entropy is the sole objective. - -### Loss preset files (`configs/model/loss/*.yaml`) - -Covered files: - -- `configs/model/loss/categorical_bce.yaml` -- `configs/model/loss/categorical_ce.yaml` -- `configs/model/loss/regression_mse.yaml` -- `configs/model/loss/regression_huber.yaml` -- `configs/model/loss/survival_coxsgd.yaml` +### Attention group (`configs/model/attention/*.yaml`) | File | Keys | Defaults | |---|---|---| -| `categorical_bce.yaml` | `_target_`, `pos_weight` | `WeightedBCEWithLogitsLoss`, `null` | -| `categorical_ce.yaml` | `_target_`, `class_weight` | `WeightedCrossEntropyLoss`, `null` | -| `regression_mse.yaml` | `_target_` | `WeightedMSELoss` | -| `regression_huber.yaml` | `_target_`, `delta` | `WeightedHuberLoss`, `1.0` | -| `survival_coxsgd.yaml` | `_target_`, `top_n`, `regularizer_weight` | `CoxSGDLoss`, `5`, `0.05` | +| `gated.yaml` | `attention_type`, `input_dim`, `hidden_dim`, `dropout`, `n_classes`, `init` | `gated`, `${model.encoder.out_dim}`, `8`, `true`, `1`, `kaiming` | +| `gated_projected.yaml` | `attention_type`, `input_dim`, `projection_dim`, `hidden_dim`, `dropout`, `n_classes`, `init` | `gated_projected`, `${model.encoder.out_dim}`, `32`, `8`, `true`, `1`, `kaiming` | -### SSL preset file (`configs/model/ssl/bgrl.yaml`) +### Losses -Keys: - -- `method` (`bgrl`) -- `predictor._target_` -- `predictor.input_size` -- `predictor.output_size` -- `predictor.hidden_size` +`task.loss` selects the loss: `categorical_bce` (`pos_weight`), `categorical_ce` +(`class_weight`, filled by `task.class_weights=auto_train`), `regression_mse`, +`regression_huber` (`delta`), `survival_coxsgd` (`top_n`, `regularizer_weight`). +Extra loss kwargs go under `model.loss.*`. ### Augmentation preset files (`configs/augmentation/*.yaml`) @@ -564,6 +573,8 @@ normalised log-degree — so hubs are preserved and low-degree nodes are masked. | `configs/scheduler/cosine_step.yaml` | `_target_` | `torch.optim.lr_scheduler.CosineAnnealingLR` | str | `grass-mil-train scheduler._target_=...` | | `configs/scheduler/cosine_step.yaml` | `T_max` | `10000` | int | `grass-mil-train scheduler.T_max=20000` | | `configs/scheduler/cosine_step.yaml` | `eta_min` | `1.0e-7` | float | `grass-mil-train scheduler.eta_min=1.0e-8` | +| `configs/scheduler/cosine_mil_step.yaml` | `T_max` | `auto` (`= max_epochs * ceil(n_train_units / hyperbatch)`, per optimizer step) | str/int | `grass-mil-train scheduler.T_max=5000` | +| `configs/scheduler/cosine_mil_step.yaml` | `eta_min` | `1.0e-7` | float | `grass-mil-train scheduler.eta_min=0` | ## 6) Trainer Config Group (`configs/trainer/*.yaml`) @@ -658,6 +669,14 @@ Covered files: | `rich_progress_bar._target_` | `lightning.pytorch.callbacks.RichProgressBar` | str | import path | `callbacks=rich_progress_bar` | `grass-mil-train callbacks.rich_progress_bar._target_=...` | | `configs/callbacks/none.yaml` | empty file | n/a | no fields | `callbacks=none` | `grass-mil-train callbacks=none` | +## 7.5 Selection presets + +| File | Content | +|---|---| +| `configs/callbacks/mil_selection.yaml` | checkpoint on `val/auroc` (max, top 1 + last), early stopping on `val/loss_region` (patience 8, after `trainer.min_epochs`) | +| `configs/callbacks/refit.yaml` | fixed-length refit: no early stopping, no metric selection, `last.ckpt` only | +| `configs/callbacks/pretrain_checkpoint.yaml` | checkpoint every `every_n_train_steps=5000`, all kept | + ## 8) Logger Config Group (`configs/logger/*.yaml`) ## 8.1 `configs/logger/csv.yaml` @@ -822,6 +841,39 @@ Covered files: | `extras.enforce_tags` | `True` | bool | `true/false` | `extras` enabled | `grass-mil-train extras.enforce_tags=false` | | `extras.print_config` | `True` | bool | `true/false` | `extras` enabled | `grass-mil-train extras.print_config=false` | +## 12a) Regime Presets (`configs/regime/*.yaml`) + +`regime=` composes `model=grass_mil`, `task=finetune_mil`, +`scheduler=cosine_mil_step`, `callbacks=mil_selection`, `data/sampler=ego_graph_mil` +and sets the keys below; each is an ordinary override afterwards. + +| Regime | `optim.lr` / `weight_decay` | `encoder_lr_scale` | head | `head_lr_scale` / `head_weight_decay` | `attention_lr_scale` | roots per region | `encoder.dropout` | drop edge / feat | epochs (min) | +|---|---|---|---|---|---|---|---|---|---| +| `mil` | 1e-4 / 1e-3 | 1.0 | MLP (2 layers) | 1.0 / null | 2.0 | 256 | 0.0 | 0 / 0 | 25 (10) | +| `mil_frozen` | 1e-4 / 1e-3 | 0.0 | MLP | 1.0 / null | 2.0 | 512 | 0.0 | 0 / 0 | 25 (10) | +| `linear_probehead` | 1e-4 / 1e-3 | 0.0 | linear, probe pre-fit, frozen | 0.0 / null | 2.0 | 512 | 0.0 | 0 / 0 | 25 (10) | +| `linear_ftall` | 1e-4 / 1e-3 | 1.0 | linear, probe pre-fit | 0.01 / 1e-3 | 2.0 | 256 | 0.0 | 0 / 0 | 25 (10) | +| `linear_ftall_reg` | 1e-4 / 1e-2 | 0.3 | linear, probe pre-fit | 0.01 / 1e-3 | 2.0 | 256 | 0.2 | 0.05 / 0.05 | 25 (10) | + +`regime=pretrain_a3` is the pretraining recipe (section 3.4): it swaps the model, task, callbacks and sampler groups and needs no labels. + +Shared by the fine-tuning regimes: `task.region_accumulation.hyperbatch_size=12`, `task.grad_clip_norm=1.0`, +`trainer.precision=16-mixed`, `data.val_sampler` = fixed 1024-root subset per +region (`fixed_per_unit_seed=0`), `data.test_sampler` = all roots. + +## 12b) Analysis CLIs + +| CLI | Config | Main keys | +|---|---|---| +| `grass-mil-probe` | `configs/probe.yaml` | `probe.roots_per_unit=96`, `probe.seed=0`, `probe.batch_size=256`, `probe.C=1.0`, `probe.class_weight=balanced`, `probe.max_iter=3000`, `probe.eval_splits=[val]`, `probe.output_path`; splits from `data.split.explicit` | +| `grass-mil-nested-cv` | `configs/nested_cv.yaml` | `nested_cv.folds_file`, `nested_cv.id_kind=region|sample|patch`, `nested_cv.{label_file,id_column,label_column,patient_column}` (default from `data.graph_labels`), `nested_cv.inner_k=3`, `inner_seed=0`, `selection_metric=val/auroc`, `epoch_reduce=median`, `min_refit_epochs=1`, `outer_folds`, `resume`, `nested_cv.probe.*` (as the probe keys, `enabled=auto`, `cache_dir`), `patient_aggregation=mean` | +| `grass-mil-extract` | `configs/extract.yaml` | `extract.nested_cv_summary` or `+extract.models.=` (+ `+extract.held_out.=[...]`), `extract.unit_id=auto|region|sample|patch`, `extract.regions`, `extract.batch_size=512`, `extract.autocast`, `extract.device`, `extract.resume`, `extract.output_dir` | +| `grass-mil-scores` | `configs/scores.yaml` | `input`, `label_column`, `scores`, `fold_column`, `patient_column`, `patient_level`, `standardise=z|rank|none`, `strata`, `n_boot=2000`, `seed=0`, `operating_rules`, `prevalences` | +| `grass-mil-niches` | `configs/niches.yaml` | `cells_dir`, `regions=held_out|all|list|file`, `labels.*`, `space=gnn|attention`, `attention_ckpt`, `reduction.{method,n_components}`, `clustering.{method,n_clusters,params.*}`, `sample_size`, `l2_normalise`, `assign_k`, `seed`, `present_threshold`, `n_boot`, `figures`, `umap.*`, `positive_class`, `positive_name` | +| `grass-mil-metaclusters` | `configs/metaclusters.yaml` | `cells_dir`, `models`, `stage=all|cluster|match|transfer|basis`, `labels.*`, the clustering keys of `niches.yaml`, `match.{distance,threshold,scan.{min,max,n},min_folds}`, `transfer.{min_cells,present_threshold}`, `basis.{enabled,space_model,k,sample_size,n_init,seed}` | +| `grass-mil-spatial-stats` | `configs/spatial_stats.yaml` | `cells_dir`, `regions`, `labels.*`, `analyses`, `group_map`, `reference`, `partners`, `infiltrating`, `nhood.{n_perms,backend}`, `cn.{window_k,n_cn,sample_size}`, `crossk.radii`, `seed`, `min_per_group` | +| `grass-mil-predict` | `configs/inference/predict.yaml` | `aggregation.mode=mean|max|none|attention_weighted|mean_probability`, `aggregation.bag_scope=patch|region|sample|patient`, `aggregation.patient_map.{label_file,id_column,patient_column}` | + ## 13) Repo-Enforced Behavioral Constraints (From Code) These constraints are not just documentation conventions; they are enforced in runtime code. diff --git a/docs/inference_utility_suite.md b/docs/inference_utility_suite.md index e1dedf2..f11fc62 100644 --- a/docs/inference_utility_suite.md +++ b/docs/inference_utility_suite.md @@ -157,5 +157,5 @@ High-value legacy utilities from `working_version` are mapped as: `collect_inference_payload` in `src/grass_mil/inference/collectors.py` - full-graph aggregation (`full_graph_*`) -> `src/grass_mil/inference/aggregation.py` - evaluation metrics (`graph_*_evaluate_fn`) -> `src/grass_mil/inference/metrics.py` -- embedding collectors -> `src/grass_mil/inference/collectors.py` + `src/grass_mil/inference/embeddings.py` +- embedding collectors -> `src/grass_mil/inference/collectors.py` + `src/grass_mil/inference/io.py` - interpretability table bridge -> `src/grass_mil/inference/interpretability_export.py` diff --git a/docs/interpretability_suite.md b/docs/interpretability_suite.md index daf6f25..098b051 100644 --- a/docs/interpretability_suite.md +++ b/docs/interpretability_suite.md @@ -107,8 +107,9 @@ Common optional columns: - `score_mode=softmax`: `score = softmax(logits)[score_logit_index]` - defaults (`sigmoid`, index `0`) keep notebook-era binary classification parity. - for survival/hazard-style interpretation, prefer `score_mode=identity`. -- `instance_attention_logits` export requires shape `(N, 1)`; multi-column attention logits are - rejected as ambiguous. +- `instance_attention_logits` with one column per class are exported as + `attention_c` (within-bag softmax) and `attention_logit_c` (raw) columns; + `attention` holds the canonical class (`attention_class_index`, default the last). Note: `niche_profiles` requires `comp_*` columns. There is no fallback to one-hot `cell_type` composition. @@ -697,10 +698,20 @@ The *algorithm* is still clustering, so `run_clustering`, `ClusteringResult` and the sklearn parameters (`n_clusters`, `min_cluster_size`) keep their names. The distinction is deliberate: those are the method, the niches are the result. +The report's clustering step also accepts the GRASS-MIL recipe used by +`grass-mil-niches`: `clustering.sample_size` (fit on a random sample), +`clustering.l2_normalise` (cluster on L2-normalised coordinates, i.e. cosine +geometry) and `clustering.assign_k` (k-NN assignment of the remaining instances). +The margin attribution plugin checks the additive identity against the bag +logits of `data.bag_table` (`grass-mil-predict` predictions) and reports the +residual. + ## What the tier-2 statistics are computed over -Every tier-2 analysis runs on the **instance graph**, not on the cell graph. -Confusing the two makes the results easy to over-read, so be explicit about it: +The instance-level tier-2 analyses run on the **instance graph**; the cell-level +analyses (next section) run on the cell graph when `data.cell_table` and +`data.cell_edge_table` are given. Confusing the two makes the results easy to +over-read, so be explicit about it: - A row of `instance_table` is one **k-hop ego-graph**, rooted at one cell. - An edge in `spatial_table` connects two **instances**, i.e. two roots. @@ -747,4 +758,3 @@ as `data.cell_table` / `data.cell_edge_table`, and these become available: Each cell inherits the niche of the ego-graph it was sampled into, so a cell appearing in several overlapping ego-graphs contributes to each. This mirrors the pooling the legacy NSCLC reports do. - diff --git a/docs/legacy_gap_audit.md b/docs/legacy_gap_audit.md deleted file mode 100644 index f0d06eb..0000000 --- a/docs/legacy_gap_audit.md +++ /dev/null @@ -1,74 +0,0 @@ -# Published-Method Fidelity Status - -This document used to assert that a list of "legacy risks" had been resolved by -rebuilding from concepts rather than porting code. That framing hid a real -problem: several load-bearing pieces of the published method were not -implemented here at all, and the audit could not have caught it, because it -checked engineering hygiene rather than fidelity to the method. - -It is now a status record. For the authoritative mapping of method → code → -test, see [`method_fidelity.md`](method_fidelity.md). - -## Gaps that were open and are now closed - -1. **The encoder could not express the published input scheme.** There was no - cell-type embedding, so cell identity — the only node input for some cohorts - — had no path into the model. Closed by `NodeInputEmbedding`. -2. **Edge conditioning was wrong.** `gine` fed the whole edge-attribute vector - into the convolution. The production models condition on the scalar edge - length. Closed by `EncoderConfig.edge_feature_index`. -3. **Attention was single-channel.** Bag pooling squeezed the head to one - channel broadcast across all class logits, so the per-class formulation — - and therefore the exact attribution identity — could not be expressed. - Closed; the identity is now asserted at `atol=1e-12`. -4. **Sampling controls were missing.** No ego-graph radius cutoff and no - interior seeding, so ego-graphs rooted at a section edge were truncated by - the boundary rather than by biology. -5. **SSL views were uniform.** Elementwise masking instead of the - degree-importance drop plus cell-size noise. -6. **No attribution.** Attention lift existed; margins, the identity check and - uncertainty intervals did not. -7. **No spatial statistics.** Moran's I, Ripley's cross-L and cross-space - agreement had no implementation. - -## Bugs found while closing them - -These produced silently wrong results rather than failures, which is why they -survived: - -1. **Sampling discarded cell type.** `_apply_transform_to_each_subgraph` dropped - every node-length tensor as "already represented by `x`" — false for - `categorical_index` and `pos`. -2. **Node codes were batched along the wrong axis.** PyTorch Geometric treats - any attribute whose name contains `index` as an edge-index tensor, so - `categorical_index` was concatenated along the feature dimension. Renamed to - `categorical_codes`; graphs from older runs are migrated on load. -3. **Attention flattening in two directions.** `inference/aggregation.py` - silently reshaped multi-column attention to 1-D while - `inference/interpretability_export.py` raised on it. One gave wrong numbers, the other - crashed. -4. **Asymmetric edge dropping.** The finetune-time transform left - `force_undirected=False` while pretraining used `True`, so the two regimes - augmented differently and message passing became direction-dependent. -5. **Order-dependent Ripley radii.** The radius grid came from whichever region - was iterated first. -6. **A concordance index that synchronised per pair.** O(n²) Python with two - `.item()` calls per pair. - -## Engineering invariants (the original content, still enforced) - -- Learnable submodules live in `nn.ModuleList`/`nn.ModuleDict`. -- Attention shape contracts are explicit; normalisation is caller-controlled and - validated by `grass_mil.contracts.validate_attention_width`. -- Samplers do not mutate datamodule state. -- Behaviour is config-driven, not hardcoded per project. The legacy - `os.environ` control surface is deliberately not ported. -- Native PyG operators are preferred; adapters stay thin. -- Optional components instantiate only when their config is set. - -## Deliberately retained - -- Region accumulation. It is a throughput mechanism rather than an objective, - and the final NSCLC runs use it (`hyperbatch_size: 16`), so it stays. -- The clustering algorithm's own names (`run_clustering`, `n_clusters`, - `min_cluster_size`). Clustering is the method; niches are its result. diff --git a/docs/method_correspondence.md b/docs/method_correspondence.md new file mode 100644 index 0000000..ce8bc82 --- /dev/null +++ b/docs/method_correspondence.md @@ -0,0 +1,52 @@ +# Method Correspondence With The Preprint + +Each item of the preprint's method maps to a preset, a module and a test. The +settings are the defaults of the presets; all remain overridable. + +| Preprint element | Default | Config | Code | Test | +|---|---|---|---|---| +| Delaunay cell graph, contact edges, bounded log edge length `clip((ln d - 2)/3, 0, 1)` | `edge_features=[log_distance, neighbor]`, `neighbor_cutoff_um` in the data's units | `data/spatial_omics.yaml` (`graph_builder`) | `data/components/graph_builders.py::log_distance_feature` | `tests/models/test_fidelity_phase1.py` | +| Node input: cell-type embedding + linear projection of continuous features, xavier init | `input_init=xavier`, `categorical_embedding` | `model/encoder/gine_length.yaml` | `models/components/embeddings.py` | phase 1 | +| GINE encoder 4 x 256, learnable epsilon, layer norm, max pooling over the ego-graph | `num_layers=4`, `hidden_dim=out_dim=256`, `train_eps=true`, `norm=layernorm`, `pooling=max` | `model/encoder/gine_length.yaml` | `models/components/backbones.py` | phase 1 | +| Ego-graphs: k = 4 hops, all neighbours, no radius cap | `depth=4`, `num_neighbors=-1`, `ego_radius.enabled=false` | `data/sampler/ego_graph_mil.yaml` | `data/components/samplers.py` | `tests/models/test_fidelity_phase2.py` | +| BGRL pretraining "A3": roots from all regions, 64 per step, AdamW 5e-5 / 1e-5, warmup 1000, 200k steps, momentum 0.99 -> 1, encoder dropout 0.1, DropImportance (mu 0.25, p_lambda 0.45) + feature noise 0.2 on size, checkpoint every 5000 steps | `regime/pretrain_a3.yaml` (alias `experiment/grass_mil_pretrain_a3.yaml`) | `model/bgrl_a3.yaml`, `task/pretrain_bgrl_a3.yaml`, `data/sampler/ego_graph_pretrain.yaml`, `callbacks/pretrain_checkpoint.yaml` | `models/bgrl_module.py`, `data/components/merge.py`, `models/training/augmentations.py` | `tests/models/test_fidelity_phase4.py` | +| Projected gated attention (32 / 8, dropout 0.25, one channel per class, kaiming) | `model/attention/gated_projected.yaml` | | `models/components/attention.py` | phase 1 | +| Logit-space pooling `L_c = sum_i A[i,c] l[i,c]`, exact per-cell contributions | `task.aggregation=mil_attention` | `task/finetune_mil.yaml` | `models/training/bagging.py`, `interpretability/core/attribution.py` | `tests/models/test_bagging.py`, phase 6 | +| One region = one bag, one softmax, one loss; class-weighted CE (train split, mean 1); fixed 1024-root validation; all-cells test | `unit_as_bag=true`, `subsample_count`, `class_weights=auto_train`, `val_sampler`, `test_sampler=null` | `regime/mil.yaml` | `models/supervised_module.py`, `models/training/class_weights.py` | phase 2 | +| Probe pre-fit: 96 roots per region, StandardScaler + LogisticRegression(C 1, balanced), folded into a linear head | `probe.yaml` | `regime/linear_*.yaml` (`task.head_init_path=auto`) | `models/training/probe.py` | `tests/models/test_fidelity_phase3.py` | +| Regimes probehead / ftALL / ftALL-reg: learning-rate scales (encoder 0 / 1 / 0.3, head 0 / 0.01, attention 2), weight decay 1e-3 / 1e-2, dropout 0.2 and edge/feature drop 0.05 in the regularised regime, bag 512 / 256, per-step cosine over the run, hyperbatch 12, clip 1, AMP, early stopping on region loss (patience 8, min 10 epochs), checkpoint on region AUROC | `regime/*.yaml`, `scheduler/cosine_mil_step.yaml`, `callbacks/mil_selection.yaml` | | `models/training/optimization.py`, `models/supervised_module.py` | phase 2 | +| Nested CV: patient-grouped outer folds, inner `StratifiedGroupKFold(3, seed 0)`, best epoch = argmax val AUROC, median refit, test once | `nested_cv.yaml` | | `nested_cv.py`, `data/components/splits.py` | phase 3 | +| Patient score: mean of core probabilities (merged bag as alternative) | `aggregation.bag_scope=patient` | `inference/aggregation/default.yaml` | `inference/aggregation.py` | `tests/models/test_fidelity_phase5.py` | +| Cell cache of every fold model (E fp16, instance logits, raw attention, bias) | `extract.yaml` | | `inference/cell_cache.py` | phase 5 | +| relattn margin `N_r [A_1 (l_1 - b_1) - A_0 (l_0 - b_0)]` | | | `interpretability/niches/margins.py` | `tests/interpretability/test_fidelity_phase6.py` | +| Cosine HDBSCAN: PCA-10 on 100k cells, L2, mcs 300, min_samples 1, eom, 15-NN assignment; niche table, co-localisation, figures | `niches.yaml` | | `interpretability/core/clustering.py::cosine_hdbscan`, `interpretability/niches/*` | phase 6 | +| Metaclusters: per-fold clustering, Hellinger distance, constrained average linkage, threshold scan or 0.40, patient shares (each core labelled once), transfer, pairwise ARI, shared k-means basis | `metaclusters.yaml` | | `interpretability/metaclusters/*` | `tests/interpretability/test_fidelity_phase7.py` | +| Bootstrap AUROC CI (2000, seed 0), DeLong, within-fold z / rank, operating points at within-fold quantile rules | `scores.yaml` | | `evaluation/*` | phase 5 | +| Neighbourhood enrichment (200 permutations), cellular neighbourhoods (10-NN windows, k-means 10 on 200k windows, CLR), contacts, cross-K (r 20/50/100, pi r^2) | `spatial_stats.yaml` | | `spatial_stats/*` | `tests/spatial_stats/test_fidelity_phase8.py` | + +## Known deviations + +- The LR warmup starts at `start_factor=0.0` through `LinearLR`, which clamps to + 1e-4 of the base rate at step 0 (the reference starts at exactly 0); the + difference is one step. +- `grass-mil-nested-cv` rejects inner-fold seeds that leave a fold without one + class and tries the next seed; on cohorts with more than a handful of patients + per class the seed is never changed. +- The niche pipeline has no clustering sweep; it reports the settings it was + given (the preprint chose its settings by a sweep once and then fixed them). +- Validation is computed on a fixed 1024-root subset per region and test on all + cells, as in the reference; both are configurable. + +## Validation against the original runs + +Recorded after the fidelity check of the public package against the preprint's +NSCLC runs (private data; the converter lives outside the repository): + +| Check | Reference | Public package | Status | +|---|---|---|---| +| Frozen embeddings of three cores (per-cell cosine with the reference fp16 cache; 875-5150 cells each) | 1.0 | median 0.993-0.995, minimum 0.97, 91-95 % of cells >= 0.99 | passed | +| Probe region AUROC, outer fold 2: inner folds 0 / 1 / 2 and refit (scored on the held-out fold) | 0.482 / 0.304 / 0.564 / 0.661 | 0.471 / 0.354 / 0.555 / 0.685 | passed (max deviation 0.05 on inner fold 1, 0.024 on the refit) | +| Held-out AUROC, `linear_ftall_reg`, outer fold 2 (inner best epochs, refit length E) | 0.664 (1 / 1 / 1, E = 1) | 0.660 (3 / 1 / 3, E = 3) | passed (deviation 0.005) | +| Held-out AUROC, `linear_probehead`, outer fold 2 (inner best epochs, refit length E) | 0.627 (10 / 14 / 1, E = 10) | 0.665 (16 / 15 / 1, E = 15) | deviation 0.038, slightly above the 0.03 target; single-seed runs on 86 held-out regions (bootstrap CI about +/- 0.12) | +| Niches of the fold-2 refit (`linear_ftall_reg`, held-out cells, default cosine HDBSCAN) | 11 clusters, noise 0.78, Spearman(emp, relattn) +0.65; hypoxic-tumour clusters most positive-associated with positive margins, lymphoid aggregates strongly negative (-1.49) | 14 clusters, noise 0.75, Spearman +0.44; hypoxic-tumour clusters most positive-associated (relattn +0.45 and +0.29), B/T-cell aggregate -1.81, noise negative | qualitatively reproduced; the public refit is a different trained model (E = 3 vs 1), so cluster boundaries differ | +| Neighbourhood-enrichment z-scores on all 426 cores (numpy backend, 200 permutations) vs the reference squidpy table: Spearman of the patient-mean z per group pair, 225 pairs | > 0.9 | 0.984 (STAS+) and 0.977 (STAS-); the same six pairs lead the ranked test table | passed | diff --git a/docs/method_fidelity.md b/docs/method_fidelity.md deleted file mode 100644 index 0d6c676..0000000 --- a/docs/method_fidelity.md +++ /dev/null @@ -1,101 +0,0 @@ -# Method Fidelity - -Where each piece of the GRASS-MIL method lives in this package, and the test -that holds it in place. This is the map to check before trusting a number. - -**The run code is the reference, not the manuscript.** Where `methods.tex` and -the final NSCLC/LUAD run code disagree, the code wins; the known divergences are -listed at the bottom. - -## Core method - -| Component | Definition | Implementation | Test | -|---|---|---|---| -| Cellular graph | Delaunay adjacency, edge distance + neighbour flag | `data/components/graph_builders.py::DelaunayGraphBuilder` | `tests/test_spatial_omics_datamodule.py` (indirect; no direct unit test) | -| Node inputs | `h⁰ = E[t] + Wₓx` — cell-type embedding **summed** with a projection of continuous features | `models/components/embeddings.py::NodeInputEmbedding` | `test_node_embeddings.py::test_node_embedding_sums_rather_than_concatenates` | -| Cell-type-only cohorts | `x` has zero columns; the embedding is the whole input | same | `test_node_embedding_supports_cell_type_only_cohorts`, `test_encoder_trains_on_cell_type_only_cohort` | -| Message passing | GINE conditioned on the **scalar edge length** only | `models/components/backbones.py::_select_edge_features` | `test_gine_reads_only_the_selected_edge_column` | -| Ego-graph extent | `r_k = radius_per_hop·k + radius_offset` | `data/components/ego_radius.py` | `test_seed_sampling.py::test_resolve_ego_radius_is_linear_in_depth` | -| Interior seeding | exclude the `n_hops` rim around the convex hull | `data/components/seed_sampling.py::convex_hull_interior_mask` | `test_interior_mask_excludes_the_hull_rim` | -| MIL attention | gated attention, softmax over instances **per class** (a binary head has two channels) | `models/components/attention.py`, `models/training/bagging.py` | `test_bagging.py::test_attention_is_normalised_per_class_over_instances` | -| Bag logit | `L_c = Σᵢ A[i,c]·ℓ[i,c]` | `models/training/bagging.py::aggregate_bag_logits_attention` | `test_bag_logit_is_the_attention_weighted_sum_of_instance_logits` | -| Objective | region cross-entropy, **sole** loss | `models/supervised_module.py::_compute_losses` | `tests/test_train_regimes.py` | -| Survival | Cox partial likelihood on a scalar log-hazard | `models/components/losses.py::CoxSGDLoss` | `test_survival_regime_fast_dev_run` | -| SSL pretraining | BGRL, symmetric cosine loss, EMA target | `models/bgrl_module.py` | `tests/test_train_regimes.py` | -| SSL views | degree-importance node/edge drop + Gaussian noise on cell size | `models/training/augmentations.py` | `tests/models/test_augmentations.py` | - -## Attribution - -The interpretability claim rests on one identity. If `identity_residual` is not -~0, every niche-level attribution derived from it is invalid. - -| Quantity | Definition | Implementation | Test | -|---|---|---|---| -| Instance contribution | `M[i,c] = A[i,c]·(ℓ[i,c] − βc)` — bias-free attention-weighted logits | `interpretability/core/attribution.py` | `test_bias_removal_shifts_the_margin_by_exactly_the_bias_term` | -| Margin (any C) | one-vs-rest: `m[i,c] = M[i,c] − mean(M[i,c'≠c])`; reduces to `M[i,1] − M[i,0]` when C=2 | `instance_ovr_margins` | `test_ovr_margin_reduces_to_the_binary_margin_when_c_is_two`, `test_ovr_margin_contrasts_each_class_against_the_mean_of_the_rest` | -| Additive identity | `Σᵢ M[i,c] = L_c` | same, `identity_residual` | `test_contributions_sum_exactly_to_the_bag_logit` | -| Uncertainty | 200-resample percentile bootstrap **over regions** | `percentile_bootstrap_ci` | `test_bootstrap_ci_brackets_the_point_estimate` | - -## Spatial statistics - -| Statistic | Implementation | Test | -|---|---|---| -| Moran's I + permutation null + BH-FDR | `interpretability/tier2/autocorrelation.py` | `test_spatial_statistics.py::test_morans_i_*` | -| Ripley cross-L, centred `L(r) − r` | `interpretability/tier2/ripley.py` | `test_ripley_l_is_near_zero_for_a_poisson_pattern` | -| Neighbourhood enrichment | `interpretability/tier2/neighborhood.py` | `test_plugins_and_tier2.py` | -| Cross-space agreement (ARI/AMI/NMI) | `interpretability/core/agreement.py` | `test_spatial_statistics.py::test_cluster_agreement_*` | - -## Defaults ordering: why `task` is merged last - -`task` is listed **after** `model`, `optim` and `scheduler` in the root defaults -lists (`train.yaml`, `eval.yaml`). This is load-bearing, not cosmetic: - -- `task/finetune_mil.yaml` sets `model.graph_head.output_dim: 2` and - `model.attention.n_classes: 2`. With `task` listed first, the module preset - merged afterwards and reset both to 1 — so `task=finetune_mil` silently ran a - single-logit head with a single attention channel, which cannot express the - attribution identity above. -- `task/pretrain_bgrl.yaml` declares `defaults: - override /scheduler: - cosine_step`. With `task` listed first, that override could not resolve and - composition failed outright with `Could not override 'scheduler'`. - -Neither failure was visible to the test suite, because the training tests build -their config by loading YAML files directly rather than composing through -Hydra, and `validate_attention_width(1, 1)` passes — one channel for one class -is internally consistent. - -`tests/configs/test_task_preset_composition.py` now composes every task preset -the way the CLI does and asserts both invariants, so a future reordering fails -loudly. - -## Known divergences from `methods.tex` - -The manuscript is stale on these points. Each is deliberate. - -1. **Edge features.** `methods.tex` describes a learned edge-**type** embedding - propagated through the conv, with the distance channel supplied but unread. - The implemented models do the opposite: GINE on the scalar edge **length**, - no edge-type notion. *The manuscript needs updating.* -2. **Degree normalisation.** `methods.tex` says min-max; the run code uses - mean-max (`(v − mean)/(max − mean)`, clamped at 0), which maps every - below-average-degree node to zero importance. Implemented as in the code. -3. **Signed margin share bounds.** `methods.tex` states this is bounded in - `[-1, 1]`. It is not: it is a fraction of the bag margin, so one niche can - exceed 1 when another opposes it. The real invariant is that a full - partition sums to 1. -4. **Moran's I scope.** The legacy report computes Moran's I over *cells* inside - pooled ego-graphs. Here the spatial table is root-instance level, so this - measures autocorrelation over the *instance* graph — a defensible and - arguably cleaner object, but not numerically the same statistic. - -## Not implemented - -Claimed in `methods.tex` but absent here, and absent from the legacy code too. -Implement or remove before submission: - -- Archetypal analysis and the three simplex tests that depend on it. -- The 10×10 ensemble, ICC(2,1)/ICC(2,k), and the two-way ANOVA variance - decomposition. All training uses a single seed; there is no ensemble harness. -- Graphlet/motif analysis and its permutation null. -- Paired bootstrap of the cross-L condition difference (the contrast is two - separately aggregated mean curves). diff --git a/docs/model_component_contracts.md b/docs/model_component_contracts.md index 4add548..40b0cb1 100644 --- a/docs/model_component_contracts.md +++ b/docs/model_component_contracts.md @@ -1,7 +1,6 @@ # Model Component Contracts This document defines the canonical contracts for model components, losses, and samplers. -These contracts are mirrored in `.cursor/rules/model_component_contracts.mdc`. ## Goals diff --git a/docs/nested_cv.md b/docs/nested_cv.md new file mode 100644 index 0000000..d89f770 --- /dev/null +++ b/docs/nested_cv.md @@ -0,0 +1,88 @@ +# Nested Cross-Validation (the preprint protocol) + +`grass-mil-nested-cv` runs the evaluation protocol of the preprint: fixed, +patient-grouped outer folds; stratified, patient-grouped inner folds for epoch +selection; a refit on the whole development set; one held-out prediction per +region and a mean per patient. `grass-mil-loocv` (leave-one-region-out) is +still available for very small cohorts, but nested CV is the protocol all +preprint numbers come from. + +## Inputs + +| Input | Form | +|---|---| +| processed graphs | any `data.processed_dir` built by `grass-mil-train` / `prepare_data` (whole regions, `data.sample_unit=full`, one graph per region) | +| labels | CSV with a region-id column, a label column (0/1 or category names) and, for patient grouping, a patient column: `data.graph_labels.{label_file,id_column,tasks,patient_column}` | +| outer folds | JSON list of lists of region ids: `nested_cv.folds_file` (patients never split across folds; build it once and version it) | +| pretrained encoder | `model.init_from_ckpt=.ckpt` (optional but recommended) | +| regime | `regime=mil` (default MLP head), `mil_frozen`, `linear_probehead`, `linear_ftall`, `linear_ftall_reg` | + +```bash +grass-mil-nested-cv regime=linear_ftall_reg \ + data.raw_manifest_path=data/raw/manifest.csv data.processed_dir=data/processed/cohort \ + data.sample_unit=full data.csv.categorical_label_columns=[cell_type] data.csv.molecular_columns=[SIZE] \ + data.graph_builder.kwargs.neighbor_cutoff_um=55 \ + data.graph_labels.label_file=data/raw/labels.csv data.graph_labels.id_column=REGION_ID \ + data.graph_labels.tasks=[STAS] data.graph_labels.patient_column=PATIENT \ + nested_cv.folds_file=data/raw/folds.json \ + model.init_from_ckpt=runs/pretrain_a3/checkpoints/last.ckpt \ + paths.output_dir=runs/nested_ftall_reg +``` + +## What happens per outer fold `i` + +1. Development set = every other fold, sorted by region id. +2. `StratifiedGroupKFold(nested_cv.inner_k, shuffle=True, random_state=nested_cv.inner_seed)` over the development regions, grouped by patient. A seed that leaves a fold without one class is rejected and the next seed tried (only matters for tiny cohorts). +3. For every inner fold: if the regime uses a probe head (`task.head_init_path: auto`, the `linear_*` regimes), `grass-mil-probe` is run on the inner training regions (cached under `nested_cv.probe.cache_dir`); then `grass-mil-train` with the regime, `callbacks=mil_selection` (checkpoint on region-level `val/auroc`, early stopping on `val/loss_region`, `trainer.min_epochs`), writing `metrics_per_epoch.jsonl`. The best epoch is `argmax val/auroc` (earliest on ties; `val/loss_region` is the fallback when AUROC is undefined). +4. `E = median` (`nested_cv.epoch_reduce`) of the inner best epochs. +5. Refit on all development regions for exactly `E` epochs (no early stopping, last weights; the probe is refitted on the development set), then test on fold `i` with every cell of every region (`test_sampler` = all roots). +6. Outputs: `outer_/result.json`, `outer_/refit/test_predictions.csv`, `outer_/oof_predictions.csv`. + +The run resumes: finished inner runs and outer folds are skipped (`nested_cv.resume=true`). + +## Outputs + +| File | Content | +|---|---| +| `nested_cv_summary.json` | `auc_mean`, `auc_sd`, `auc_folds`, per-fold `E`, inner epochs, run directories, pooled region AUROC, patient AUROC | +| `oof_predictions.csv` | one row per region: `outer`, `region_id`, `patient_id`, `label`, `logit_c`, `prob_class_c` | +| `oof_patient_predictions.csv` | mean of the region probabilities per patient | +| `probes/*.pt` | folded probe heads (`graph_head.net.0.{weight,bias}`) per split | + +Score the table with `grass-mil-scores input=/oof_predictions.csv` (bootstrap +CI, per-fold mean, operating points) and extract cells of the five refit models +with `grass-mil-extract extract.nested_cv_summary=/nested_cv_summary.json`. + +## Regimes + +| `regime=` | encoder | instance head | attention | bag | notes | +|---|---|---|---|---|---| +| `mil` (default) | fine-tuned (lr x1) | MLP (2 layers), trained | lr x2 | 256 roots | GRASS-MIL default | +| `mil_frozen` | frozen | MLP | lr x2 | 512 | encoder in eval mode, no gradients | +| `linear_probehead` | frozen | linear, pre-fitted by the probe, frozen | lr x2 | 512 | attention is the only trainable part | +| `linear_ftall` | fine-tuned (lr x1) | linear, pre-fitted, lr x0.01, wd 1e-3 | lr x2 | 256 | | +| `linear_ftall_reg` | fine-tuned (lr x0.3) | linear, pre-fitted, lr x0.01 | lr x2 | 256 | wd 1e-2, encoder dropout 0.2, edge/feature drop 0.05 | + +All regimes share AdamW (lr 1e-4), a per-step cosine schedule over the whole run +(`scheduler=cosine_mil_step`, `T_max: auto`), inverse-frequency class weights from +the training regions, gradient accumulation over 12 regions, gradient clipping +at 1, mixed precision, a fixed 1024-root validation subset per region and +all-cells testing. Every value is a Hydra key, e.g. `regime=mil +task.optimization.encoder_lr_scale=0.3 data.sampler.runtime.subsample_count=128 +trainer.max_epochs=40 model.encoder.num_layers=3 data.sampler.runtime.depth=3`. + +## Patient-level scores + +`oof_patient_predictions.csv` averages the region probabilities (the preprint +default). The merged-bag alternative, where all cells of a patient's regions +form one bag with a joint attention softmax, is available at prediction time: + +```bash +grass-mil-predict ckpt_path=/checkpoints/last.ckpt regime=linear_ftall_reg data=... \ + aggregation.mode=attention_weighted aggregation.bag_scope=patient \ + aggregation.patient_map.label_file=labels.csv aggregation.patient_map.id_column=REGION_ID \ + aggregation.patient_map.patient_column=PATIENT +``` + +`aggregation.mode=mean_probability aggregation.bag_scope=patient` reproduces the +mean-of-regions score from the same run. diff --git a/docs/niche_discovery.md b/docs/niche_discovery.md new file mode 100644 index 0000000..250d978 --- /dev/null +++ b/docs/niche_discovery.md @@ -0,0 +1,89 @@ +# Niche Discovery And Cross-Fold Replication + +Everything downstream of a trained model works on its **cell cache**: for every +cell of every region, the encoder embedding of the ego-graph rooted at that +cell (`E`, fp16), the per-class instance logits (`LG`) and the raw per-class +attention logits (`AR`), plus the head bias, cell types, positions and the cell +graph once per region. + +## 1. Extract the cells + +```bash +# every refit model of a nested CV (held-out regions recorded per model) +grass-mil-extract regime=linear_ftall_reg data=... \ + extract.nested_cv_summary=runs/nested_ftall_reg/nested_cv_summary.json \ + paths.output_dir=runs/cells + +# one explicit checkpoint +grass-mil-extract regime=mil data=... +extract.models.final=runs/train/checkpoints/last.ckpt +``` + +Output: `/cells//index.json` + one `.npz` per region. +The model is composed from the same `regime`/`model` keys as the training run so +the checkpoint fits; ego-graph settings come from `data.sampler.runtime`. + +## 2. Niches of one model (`grass-mil-niches`) + +```bash +grass-mil-niches cells_dir=runs/cells/fold2 output_dir=runs/niches_fold2 \ + positive_name=STAS held_out_auroc=0.664 +``` + +Default settings (the preprint recipe; every key is overridable, no sweep): + +| key | default | meaning | +|---|---|---| +| `regions` | `held_out` | cells of the regions the model never trained on (`all`, a list or a file are accepted) | +| `space` | `gnn` | encoder embedding; `attention` = the 8-d pre-tanh attention activation (`attention_ckpt`) | +| `reduction` | PCA, 10 components | fitted on a random sample of `sample_size` cells | +| `l2_normalise` | true | cluster on L2-normalised PCs, i.e. the cosine geometry | +| `clustering` | HDBSCAN, `min_cluster_size` 300, `min_samples` 1, `eom` | any backend of the clustering registry (`kmeans` with `n_clusters`, `agglomerative`, `dbscan`, `optics`, `spectral`) | +| `assign_k` | 15 | k-NN assignment of unsampled cells on the PCs (backends with `predict`, e.g. k-means, assign directly) | +| `sample_size` | 100000 | cells used to fit reducer and clustering (`null` = all) | + +Per cell, the **attention-weighted margin** is +`relattn_i = N_r [A_{i,1}(l_{i,1} - b_1) - A_{i,0}(l_{i,0} - b_0)]` +with the attention normalised over the region's cells per class, so that the +margins of a region sum to `N_r` times its bias-free bag margin. + +Outputs: `labels.npy`, `cluster_model.pkl` (joblib; reducer + assigner, reusable +on other cells), `cells.csv` (region, cell index, cluster, cell type, relattn, +x, y), `clusters.csv`, `coloc_log2.npy`, `report.md`, `pca_triptych.png`, +`umap_triptych.png` (if `umap-learn` is installed), `clusters_overview.png`, +`colocalisation.png`. + +`clusters.csv` columns: share of cells, regions present (> `present_threshold`), +positive / negative regions present, composition (`comp_`), top types, +enriched types (> 1.5x the cohort frequency), empirical fraction of cells from +positive regions, AUROC of the per-region share, `relattn` with a bootstrap CI +over regions (`n_boot`), signed share of the region margin, sign consistency, +attention lift. The report adds the Spearman concordance between empirical +association and margin and the co-localisation `log2(obs/exp)` of cell-cell edges +between clusters. + +## 3. Cross-fold replication (`grass-mil-metaclusters`) + +A clustering of one model lives in that model's space. To ask which niches +replicate, cluster every fold model's held-out cells with the same fixed +settings and match the clusters across folds by composition: + +```bash +grass-mil-metaclusters cells_dir=runs/cells output_dir=runs/meta \ + labels.label_file=data/raw/labels.csv labels.label_column=STAS labels.patient_column=PATIENT \ + positive_name=STAS +``` + +Stages (`stage=all|cluster|match|transfer|basis`): + +1. **cluster**: per fold model, `grass-mil-niches` settings on its held-out cells -> `cluster/fold_clusters.csv`, `labels_.npy`, `cluster_model_.pkl`. +2. **match**: Hellinger distance between cluster compositions (`match.distance=aitchison` for CLR euclidean), constrained average linkage that never merges two clusters of the same fold, threshold from a scan maximising the number of metaclusters replicated in `>= match.min_folds` folds (ties -> fewer metaclusters) or fixed (`match.threshold=0.4`). Outputs `match/metaclusters.csv` (members, within distance, per-fold empirical association and margin, sign agreement, CI exclusion count), `metacluster_patient_shares.csv` (each region labelled once, by the model that held it out) and `metacluster_patient_tests.csv` (Mann-Whitney + BH, AUROC of the share), `report.md`, `metaclusters_overview.png`. +3. **transfer**: every model labels every region in its own space with its saved cluster model; `transfer/transfer_meta_by_model.csv` (share, association, margin on train vs held-out regions per metacluster and model), `transfer_model_ari.csv` (pairwise ARI on cells both models assign), consensus patient shares and tests. +4. **basis**: a shared label-free k-means basis (`basis.k`, standardised embeddings of `basis.space_model`, `basis.sample_size` cells) with the attribution of every fold model split into held-out / train / all (model, region) pairs: `basis/basis_attribution.csv`, `report_basis.md`, `basis_attribution.png`. + +## 4. The interpretability report (`grass-mil-report`) + +The HTML/PDF report of `docs/interpretability_suite.md` works on instance +tables exported by `grass-mil-predict`. Its clustering step accepts the same +sample / L2 / assignment recipe (`clustering.sample_size`, +`clustering.l2_normalise`, `clustering.assign_k`) and its margin attribution +checks the additive identity against the bag table (`data.bag_table`). diff --git a/docs/repository_map.md b/docs/repository_map.md index e6c3bef..2c864c4 100644 --- a/docs/repository_map.md +++ b/docs/repository_map.md @@ -1,433 +1,379 @@ # Repository Map -This document maps tracked files in the repository root to their responsibilities. +Every tracked file (`git ls-files`) with a one-line purpose. Regenerate with +`python tools/update_repository_map.py` after adding files. -Scope rules used here: - -1. Source of truth is `git ls-files` (tracked files only). -2. Generated/runtime artifacts are documented separately at the end. -3. Every tracked file is listed with one-line purpose and usage context. - -## Root Metadata And Project Tooling +## Root metadata and tooling +- `.gitignore`: Ignore rules for generated artefacts, environments, logs, data and vignette outputs. +- `.pre-commit-config.yaml`: Code-quality hooks (ruff lint + format, file hygiene checks). - `.project-root`: Root marker used by `rootutils.setup_root(...)`. -- `.gitignore`: Ignore rules for generated artifacts, envs, logs, and data outputs. -- `.env.example`: Example environment variable file loaded by Hydra/OmegaConf env references. -- `.pre-commit-config.yaml`: Code quality hooks (ruff lint + format, plus basic file hygiene checks). -- `LICENSE`: Project license text. -- `README.md`: Primary entrypoint documentation and navigation index. -- `Makefile`: Convenience targets for clean, format, tests, and default training. -- `pyproject.toml`: Package metadata, dependencies, optional extras, console - entry points (`grass-mil-train`, `-eval`, `-loocv`, `-predict`, `-report`), - and pytest/coverage/ruff configuration. -- `environment.yaml`: Conda environment alternative for local development, pinned to major versions. CI does not use it; the CI stack is pinned inline in `.github/workflows/test.yml`. - -## CI/DevOps And Contribution Files - -- `.github/PULL_REQUEST_TEMPLATE.md`: Pull-request submission template. -- `.github/codecov.yml`: Codecov upload/report behavior. +- `CHANGELOG.md`: Release notes. +- `CITATION.cff`: Machine-readable citation (preprint DOI 10.64898/2026.10.08.757722). +- `LICENSE`: MIT license. +- `Makefile`: Convenience targets: `test`, `format`, `clean`, `clean-logs`. +- `README.md`: Landing page: method summary, install, typical usage, workflow table, citation. +- `environment.yaml`: Conda environment alternative for local development. +- `pyproject.toml`: Package metadata, dependencies, extras, console entry points (`grass-mil-*`), pytest/ruff configuration. + +## CI and contribution files + +- `.github/PULL_REQUEST_TEMPLATE.md`: Pull-request template. +- `.github/codecov.yml`: Codecov configuration. - `.github/dependabot.yml`: Dependabot update policy. - `.github/release-drafter.yml`: Release drafter configuration. -- `.github/workflows/code-quality-main.yaml`: Pre-commit quality checks on pushes to `main`. -- `.github/workflows/code-quality-pr.yaml`: Pre-commit quality checks on changed PR files. -- `.github/workflows/release-drafter.yml`: Automated draft release workflow. -- `.github/workflows/test.yml`: Multi-OS (Linux/macOS) test workflow, plus coverage and a from-source packaging job. - -## Rules And Contracts - -- `.cursor/rules/model_component_contracts.mdc`: Contract rules for model/loss/sampler architecture constraints. - -## Config System (`configs/`) - -> Throughout this document `configs/...` is shorthand for -> `src/grass_mil/configs/...`. The config tree lives inside the package so it -> ships with the wheel; Hydra resolves it relative to the entrypoint module. - -### Config Package Marker - -- `configs/__init__.py`: Marks config directory as package context. - -### Top-Level Compositions - -- `configs/train.yaml`: Primary training composition and defaults chain. -- `configs/eval.yaml`: Primary evaluation composition and defaults chain. -- `configs/loocv.yaml`: LOOCV composition for single-fold and full-loop orchestration. - -### Callback Config Group - -- `configs/callbacks/default.yaml`: Default callback bundle composition. -- `configs/callbacks/model_checkpoint.yaml`: Checkpoint callback schema/default fields. -- `configs/callbacks/early_stopping.yaml`: Early stopping callback schema/default fields. -- `configs/callbacks/model_summary.yaml`: Rich model summary callback config. -- `configs/callbacks/rich_progress_bar.yaml`: Rich progress bar callback config. -- `configs/callbacks/none.yaml`: Empty callback preset to disable callbacks. - -### Data Config Group - -- `configs/data/spatial_omics.yaml`: Main datamodule and preprocessing/data-loading parameters. - -Sampler subgroup: - -- `configs/data/sampler/identity.yaml`: Identity batching strategy preset. -- `configs/data/sampler/shadow_native.yaml`: Native PyG ShaDow sampling preset. -- `configs/data/sampler/shadow_custom.yaml`: Custom ShaDow sampling preset with transform support. - -### Debug Config Group - -- `configs/debug/default.yaml`: Debug baseline (CPU, no logger/callbacks, anomaly detection). -- `configs/debug/fdr.yaml`: Fast-dev-run debug preset. -- `configs/debug/limit.yaml`: Subset-batch debug preset. -- `configs/debug/overfit.yaml`: Overfit-a-few-batches debug preset. -- `configs/debug/profiler.yaml`: Profiling-enabled debug preset. - -### Experiment Config Group - -- `configs/experiment/debug.yaml`: Dummy-data composition for notebook/debug experiments. - -### Extras/Hydra/Paths Groups - -- `configs/extras/default.yaml`: Run-time UX toggles (warnings, tags, config printing). -- `configs/hydra/default.yaml`: Hydra logging and output directory templates. -- `configs/paths/default.yaml`: Path roots, data dir, log dir, output dir, work dir. -- `configs/local/.gitkeep`: Placeholder for untracked machine-local overrides. - -### Logger Config Group - -- `configs/logger/csv.yaml`: CSV logger preset. -- `configs/logger/tensorboard.yaml`: TensorBoard logger preset. -- `configs/logger/wandb.yaml`: Weights & Biases logger preset. -- `configs/logger/mlflow.yaml`: MLflow logger preset. -- `configs/logger/comet.yaml`: Comet logger preset. -- `configs/logger/neptune.yaml`: Neptune logger preset. -- `configs/logger/aim.yaml`: Aim logger preset. -- `configs/logger/many_loggers.yaml`: Multi-logger composition preset. - -### Model Config Group - -Root model module configs: - -- `configs/model/supervised_module.yaml`: Unified supervised module defaults (mean/MIL via task config). -- `configs/model/bgrl_module.yaml`: BGRL pretraining module defaults. -- `configs/model/components.yaml`: Component-instantiation config used by the component tests. - -Encoder presets: - -- `configs/model/encoder/gin.yaml`: GIN encoder preset. -- `configs/model/encoder/gcn.yaml`: GCN encoder preset. -- `configs/model/encoder/gat.yaml`: GAT encoder preset. -- `configs/model/encoder/graphsage.yaml`: GraphSAGE encoder preset. -- `configs/model/encoder/gine.yaml`: GINE encoder preset with edge feature dimension. -- `configs/model/encoder/gine_length.yaml`: Production encoder — GINE on the scalar edge length, LayerNorm, cell-type embedding. - -Attention presets: - -- `configs/model/attention/gated.yaml`: Gated MIL attention preset. -- `configs/model/attention/gated_projected.yaml`: Projected gated MIL attention preset. - -Head presets: - -- `configs/model/heads/graph.yaml`: Graph-level prediction MLP head preset. - -Loss presets: - -- `configs/model/loss/categorical_bce.yaml`: Binary BCE-with-logits loss preset. -- `configs/model/loss/categorical_ce.yaml`: Multiclass cross-entropy loss preset. -- `configs/model/loss/regression_mse.yaml`: MSE regression loss preset. -- `configs/model/loss/regression_huber.yaml`: Huber regression loss preset. -- `configs/model/loss/survival_coxsgd.yaml`: Cox-style survival loss preset. - -SSL preset: - -- `configs/model/ssl/bgrl.yaml`: BGRL SSL predictor config preset. - -### Optimization And Scheduling Groups - -- `configs/optim/adamw.yaml`: Default AdamW optimizer preset. -- `configs/scheduler/cosine_epoch.yaml`: Epoch-interval cosine scheduler preset. -- `configs/scheduler/cosine_step.yaml`: Step-interval cosine scheduler preset. - -### Task Group - -- `configs/task/finetune_mean.yaml`: Supervised mean-aggregation task preset. -- `configs/task/finetune_mil.yaml`: Supervised MIL-attention task preset with optional MIL extras. -- `configs/task/finetune_survival.yaml`: Cox survival task preset (scalar log-hazard, c-index). -- `configs/task/pretrain_bgrl.yaml`: SSL BGRL pretraining task preset. - -### Augmentation Group - -- `configs/augmentation/bgrl_paper.yaml`: Degree-importance view generator (production). -- `configs/augmentation/uniform.yaml`: Structure-independent uniform drop baseline. - -### Inference Group - -- `configs/inference/predict.yaml`: Prediction entrypoint composition. -- `configs/inference/aggregation/default.yaml`: Bag aggregation mode, scope, and subsampling. -- `configs/inference/embeddings/default.yaml`: Embedding export toggles. -- `configs/inference/interpretability/default.yaml`: Instance/spatial table export toggles. -- `configs/inference/metrics/default.yaml`: Task-aware metric selection. - -### Interpretability Group - -- `configs/interpretability/report.yaml`: Report entrypoint composition. -- `configs/interpretability/reduction/{pca,tsne,umap}.yaml`: Dimensionality-reduction presets. -- `configs/interpretability/clustering/{agglomerative,dbscan,hdbscan,kmeans,optics,spectral}.yaml`: Clustering presets. -- `configs/interpretability/plugins/default.yaml`: Minimal plugin set (`niche_profiles`). -- `configs/interpretability/plugins/full.yaml`: All inference-only plugins enabled. - -### Trainer Group - -- `configs/trainer/default.yaml`: Base Lightning Trainer defaults. -- `configs/trainer/cpu.yaml`: CPU runtime preset. -- `configs/trainer/gpu.yaml`: Single-GPU preset. -- `configs/trainer/mps.yaml`: Apple MPS runtime preset. -- `configs/trainer/ddp.yaml`: Multi-GPU DDP preset. -- `configs/trainer/ddp_sim.yaml`: CPU `ddp_spawn` simulation preset. - -## Source Code (`src/grass_mil/`) - -### Package Marker - -- `src/grass_mil/__init__.py`: Package marker. -- `src/grass_mil/_paths.py`: Best-effort `PROJECT_ROOT` resolution for the CLI entrypoints. -- `src/grass_mil/configs/`: Hydra config tree, shipped as package data. -- `src/grass_mil/contracts.py`: Package-level contracts — attention-width validation and interpretability dataclasses. - -### Entrypoints - -- `src/grass_mil/train.py`: Hydra training entrypoint (train + optional test). -- `src/grass_mil/eval.py`: Hydra evaluation entrypoint (test from checkpoint). -- `src/grass_mil/loocv.py`: Hydra LOOCV entrypoint with fold discovery, per-fold runs, and aggregate summaries. - -### Data Pipeline (`src/grass_mil/data/`) - -- `src/grass_mil/data/__init__.py`: Data package marker. -- `src/grass_mil/data/spatial_omics_datamodule.py`: LightningDataModule wiring preprocessing, splitting, and loaders. - -Data components: - -- `src/grass_mil/data/components/__init__.py`: Data components package marker. -- `src/grass_mil/data/components/spatial_types.py`: Canonical in-memory spatial table dataclass. -- `src/grass_mil/data/components/loaders.py`: CSV/H5AD/SCE loaders and polygon parsing utilities. -- `src/grass_mil/data/components/patching.py`: Polygon/tile patch extraction helpers. -- `src/grass_mil/data/components/graph_builders.py`: Graph builder interfaces and Delaunay implementation. -- `src/grass_mil/data/components/feature_reducers.py`: Feature reducer interfaces and implementations (`identity`, `pca`). -- `src/grass_mil/data/components/precompute.py`: Precompute orchestration into persisted PyG graph artifacts/indexes. -- `src/grass_mil/data/components/datasets.py`: Dataset wrappers for persisted graph indices and transform application. -- `src/grass_mil/data/components/samplers.py`: Batch/sampler strategies (identity, shadow_native, shadow_custom). -- `src/grass_mil/data/components/transforms.py`: Transform instantiation and composition-vector transform. -- `src/grass_mil/data/components/seed_sampling.py`: Convex-hull interior seed masks with on-disk cache. -- `src/grass_mil/data/components/ego_radius.py`: Physical radius cutoff for sampled ego-graphs. - -### Model Runtime (`src/grass_mil/models/`) - -- `src/grass_mil/models/__init__.py`: Model package marker. -- `src/grass_mil/models/supervised_module.py`: Unified supervised training module (mean + MIL behavior). -- `src/grass_mil/models/bgrl_module.py`: BGRL self-supervised training module. - -Model components: - -- `src/grass_mil/models/components/__init__.py`: Component package exports. -- `src/grass_mil/models/components/backbones.py`: GNN encoder definitions and configuration contract. -- `src/grass_mil/models/components/pooling.py`: Graph pooling wrapper. -- `src/grass_mil/models/components/attention.py`: MIL attention blocks. -- `src/grass_mil/models/components/heads.py`: Graph-level MLP prediction head. (The node head was removed with the instance-pull auxiliary loss.) -- `src/grass_mil/models/components/losses.py`: Weighted loss implementations (classification/regression/survival). -- `src/grass_mil/models/components/ssl.py`: BGRL and predictor modules. -- `src/grass_mil/models/components/factory.py`: Factory builders for encoder/attention/loss/head/SSL modules. -- `src/grass_mil/models/components/embeddings.py`: `NodeInputEmbedding` — cell-type embedding summed with the projected features. - -Training helpers: - -- `src/grass_mil/models/training/__init__.py`: Training-helper exports. -- `src/grass_mil/models/training/builders.py`: Component build/inference helpers and task validation. -- `src/grass_mil/models/training/bagging.py`: Bag ID resolution, instance grouping/sampling, bag aggregation logic. -- `src/grass_mil/models/training/loss_utils.py`: Loss routing and accuracy helpers. (Auxiliary/regularisation terms were removed; region cross-entropy is the sole objective.) -- `src/grass_mil/models/training/optimization.py`: Optimizer/scheduler instantiation and warmup wrapper. -- `src/grass_mil/models/training/checkpoint_init.py`: Checkpoint loading and encoder-key remapping. -- `src/grass_mil/models/training/augmentations.py`: BGRL view generation (degree-importance drop, edge drop, feature noise). -- `src/grass_mil/models/training/ssl_runtime.py`: `CosineWarmup` momentum/LR schedule helper. (View augmentations live in `augmentations.py`.) - -### Inference (`src/grass_mil/inference/`) - -- `src/grass_mil/inference/__init__.py`: Inference package exports. -- `src/grass_mil/inference/predict.py`: Hydra prediction entrypoint; writes predictions, metrics, embeddings and interpretability tables. -- `src/grass_mil/inference/collectors.py`: Normalises `predict_step` outputs into a single payload. -- `src/grass_mil/inference/aggregation.py`: Bag/region aggregation of instance logits (mean, max, attention-weighted). -- `src/grass_mil/inference/metrics.py`: Task-aware metrics, including the chunked concordance index. -- `src/grass_mil/inference/embeddings.py`: Graph/node embedding tables from the collected payload. -- `src/grass_mil/inference/interpretability_export.py`: Builds `instance_table` and `spatial_table` for the report tool. -- `src/grass_mil/inference/schemas.py`: Output table schemas and column contracts. -- `src/grass_mil/inference/io.py`: JSON/CSV writers with non-finite sanitisation. - -### Interpretability (`src/grass_mil/interpretability/`) - -- `src/grass_mil/interpretability/__init__.py`: Public interpretability API surface. -- `src/grass_mil/interpretability/pipeline.py`: Reduction → clustering → niche ordering → plugin execution. -- `src/grass_mil/interpretability/report_cli.py`: Hydra report entrypoint (`grass-mil-report`). - -Core analysis: - -- `src/grass_mil/interpretability/core/__init__.py`: Core exports. -- `src/grass_mil/interpretability/core/data.py`: Table loading and embedding-set extraction. -- `src/grass_mil/interpretability/core/reduction.py`: PCA/UMAP/t-SNE reduction. -- `src/grass_mil/interpretability/core/clustering.py`: Clustering backends. -- `src/grass_mil/interpretability/core/niches.py`: Background labelling, dendrogram ordering, niche renumbering. -- `src/grass_mil/interpretability/core/biomarkers.py`: Per-niche composition summaries. -- `src/grass_mil/interpretability/core/attribution.py`: Attention-weighted logit margins, OvR contrasts, bootstrap CIs, identity residual. -- `src/grass_mil/interpretability/core/agreement.py`: Cross-space agreement (ARI/AMI/NMI) and cluster Jaccard. -- `src/grass_mil/interpretability/core/transfer.py`: Niche label transfer to new cohorts via PCA+kNN. - -Tier-2 spatial statistics: - -- `src/grass_mil/interpretability/tier2/__init__.py`: Tier-2 exports. -- `src/grass_mil/interpretability/tier2/neighborhood.py`: Neighbourhood enrichment and differential enrichment. -- `src/grass_mil/interpretability/tier2/filtration.py`: Filtration curves over distance thresholds. -- `src/grass_mil/interpretability/tier2/autocorrelation.py`: Moran's I with permutation null and BH-FDR. -- `src/grass_mil/interpretability/tier2/ripley.py`: Ripley cross-L with self-pair correction. -- `src/grass_mil/interpretability/tier2/tissue_graph.py`: Tissue-graph view preparation and figure building. - -Plugins: - -- `src/grass_mil/interpretability/plugins/__init__.py`: Plugin exports. -- `src/grass_mil/interpretability/plugins/base.py`: Plugin protocol, context and result types. -- `src/grass_mil/interpretability/plugins/registry.py`: Name-keyed plugin registry. -- `src/grass_mil/interpretability/plugins/builtin.py`: The ten built-in plugins. - -Reporting: - -- `src/grass_mil/interpretability/reporting/__init__.py`: Reporting exports. -- `src/grass_mil/interpretability/reporting/plotly_builders.py`: Figure builders. -- `src/grass_mil/interpretability/reporting/sections.py`: Section assembly from plugin results. -- `src/grass_mil/interpretability/reporting/html.py`: HTML document rendering. -- `src/grass_mil/interpretability/reporting/snapshot.py`: Static image export for PDF. -- `src/grass_mil/interpretability/reporting/pdf.py`: PDF rendering backend. -- `src/grass_mil/interpretability/reporting/render.py`: Render orchestration and graceful degradation. - -### Utilities (`src/grass_mil/utils/`) - -- `src/grass_mil/utils/__init__.py`: Utility export surface. -- `src/grass_mil/utils/pylogger.py`: Rank-aware logger adapter. -- `src/grass_mil/utils/instantiators.py`: Hydra callback/logger instantiation helpers. -- `src/grass_mil/utils/logging_utils.py`: Hyperparameter logging packaging for logger backends. -- `src/grass_mil/utils/rich_utils.py`: Rich config-tree and tag prompting utilities. -- `src/grass_mil/utils/utils.py`: Misc execution helpers (`extras`, `task_wrapper`, metric extraction). - -## Tests (`tests/`) - -### Package/Fixture Layer - -- `tests/__init__.py`: Tests package marker. -- `tests/conftest.py`: Shared Hydra config fixtures for train/eval tests. -- `tests/helpers/config_paths.py`: Resolves the packaged config tree for tests. - -### Top-Level Integration/Config Tests - -- `tests/test_configs.py`: Compose/instantiate sanity tests for train/eval configs. -- `tests/test_train.py`: Core training loop behavior tests (fast-dev, resume, DDP sim, etc.). -- `tests/test_eval.py`: Train-then-eval integration test. -- `tests/test_train_regimes.py`: Regime matrix tests (BGRL, mean, MIL, SSL->finetune). -- `tests/test_spatial_omics_datamodule.py`: Data preprocessing and split persistence behavior tests. -- `tests/test_loocv.py`: Fold discovery, isolation-flag safety, and per-fold run tests. -- `tests/test_packaging.py`: Installed-package import, shipped configs, and console-script contract tests. - -### Config Contract Tests - -- `tests/configs/test_component_instantiation.py`: YAML component and sampler resolution tests. - -### Data Tests - -- `tests/data/test_samplers.py`: Identity and shadow sampler behavior/edge-case tests. -- `tests/data/test_datamodule_sampler_exposure.py`: Datamodule sampler strategy exposure tests. -- `tests/data/test_precompute_reducer_modes.py`: Reducer fit-mode semantics (`global`, `train_only`) tests. -- `tests/data/test_precompute_manifest_regions.py`: Manifest region-id uniqueness validation tests. -- `tests/data/test_loader_flags.py`: Loader flag propagation tests. -- `tests/data/test_seed_sampling.py`: Interior seed masks and ego-radius cutoff tests. - -### Model Tests - -- `tests/models/test_backbones.py`: Backbone forward and JK mode tests. -- `tests/models/test_attention.py`: Attention shape and factory optionality tests. -- `tests/models/test_losses.py`: Loss finite-output and edge-case tests. -- `tests/models/test_ssl.py`: SSL/BGRL forward/update and factory validation tests. -- `tests/models/test_optimization.py`: Warmup/scheduler and multi-LR optimizer-group tests. -- `tests/models/test_runtime.py`: Runtime helper behavior and MIL buffering/predict-step tests. -- `tests/models/test_bagging.py`: Per-class attention normalisation and the bag-logit identity. -- `tests/models/test_node_embeddings.py`: Cell-type embedding, zero-`input_dim`, and edge-column selection tests. -- `tests/models/test_augmentations.py`: BGRL view generation determinism and hyperparameter validation. - -### Inference Tests - -- `tests/eval/test_predict_cli.py`: Prediction entrypoint smoke and subsampling-plan tests. -- `tests/eval/test_aggregation.py`: Bag/region aggregation across modes and scopes. -- `tests/eval/test_collectors.py`: Payload normalisation and required-field errors. -- `tests/eval/test_metrics.py`: Task metrics, including concordance-index correctness and speed. -- `tests/eval/test_embeddings.py`: Embedding payload to DataFrame conversion. -- `tests/eval/test_interpretability_export.py`: Instance/spatial table construction and attention columns. -- `tests/eval/test_io.py`: JSON writer non-finite sanitisation. - -### Interpretability Tests - -- `tests/interpretability/test_core.py`: Reduction, clustering and composition-summary determinism. -- `tests/interpretability/test_niches.py`: Background labelling, dendrogram ordering and renumbering. -- `tests/interpretability/test_attribution.py`: Margin attribution, OvR contrasts, identity residual, bootstrap CIs. -- `tests/interpretability/test_spatial_statistics.py`: Moran's I, Ripley cross-L and cluster agreement. -- `tests/interpretability/test_plugins_and_tier2.py`: Plugin registry and tier-2 analysis behaviour. -- `tests/interpretability/test_pipeline_and_report.py`: End-to-end pipeline and report rendering. -- `tests/interpretability/test_tissue_graph.py`: Tissue-graph view preparation and figure tests. -- `tests/interpretability/test_transfer.py`: Niche transfer fit/apply and bundle round-trip tests. - -### Contract Tests - -- `tests/contracts/test_model_component_contracts.py`: Presence/consistency checks for documented component contracts. - -### Test Helper Utilities - -- `tests/helpers/__init__.py`: Helper package marker. -- `tests/helpers/package_available.py`: Dependency availability probes for conditional tests. -- `tests/helpers/run_if.py`: Conditional marker helper for environment-gated tests. -- `tests/helpers/run_sh_command.py`: Shell-command helper for tests. -- `tests/helpers/synthetic_datamodule.py`: Synthetic datamodule fixture for fast isolated training tests. - -## Scripts, Docs, Notebooks, And Placeholders - -### Scripts - -- `scripts/schedule.sh`: Example sequential training-run script. - -### Documentation - -- `docs/method_fidelity.md`: Method → code → test map, known divergences from `methods.tex`, and known defects. -- `docs/legacy_gap_audit.md`: Status record of closed fidelity gaps and the bugs found closing them. -- `docs/model_component_contracts.md`: Canonical component contracts for model/sampler/loss behavior. -- `docs/repository_map.md`: This tracked-file responsibility map. -- `docs/dataset_preparation.md`: Manifest and input-schema preparation guide. -- `docs/workflows_train_infer.md`: End-to-end training/inference runbooks. -- `docs/hyperparameter_reference.md`: Exhaustive reference of repo-defined configuration parameters. -- `docs/capabilities_and_modes.md`: Operational capability matrix and mode catalog. -- `docs/inference_utility_suite.md`: Prediction outputs, aggregation modes and exported tables. -- `docs/interpretability_suite.md`: Interpretability architecture, contracts and plugin catalog. -- `docs/interpretability_extension_guide.md`: How to add a new analysis plugin. - -### Vignettes - -- `vignettes/README.md`: Index, dataset description and honest scope notes. -- `vignettes/01_anndata_to_spatial_graphs.ipynb`: squidpy AnnData to manifest to cellular graphs. -- `vignettes/02_pretraining_and_niches.ipynb`: BGRL pretraining, embedding export and niche discovery. - -### Data/Logs Placeholders - -- `data/.gitkeep`: Placeholder to keep data directory tracked. -- `logs/.gitkeep`: Placeholder to keep logs directory tracked. - -## Generated/Runtime Artifacts (Not Part Of Tracked Functional Map) - -These are intentionally excluded from the tracked functional map above and are typically ignored or environment-specific: - -- `.git/` internals (VCS metadata and object store) -- `__pycache__/` and `*.pyc` -- `.pytest_cache/` -- Hydra run directories under `logs//runs/*` and `logs//multiruns/*` -- Precomputed runtime outputs under `data/processed/*` (unless explicitly committed by the repository owner) -- Local virtual environments (`.venv`, `venv`, etc.) - -Use `git ls-files` to regenerate the tracked-only baseline when the repository changes. +- `.github/workflows/code-quality-main.yaml`: CI: pre-commit on every push to main. +- `.github/workflows/code-quality-pr.yaml`: CI: pre-commit on changed files of a pull request. +- `.github/workflows/release-drafter.yml`: CI: draft release notes from merged pull requests. +- `.github/workflows/test.yml`: CI: pytest on Linux and macOS, coverage, install-from-source smoke test. +- `.vscode/settings.json`: Editor settings. + +## Maintenance tools + +- `tools/update_repository_map.py`: Regenerates this map. + +## Package: entry points and shared modules + +- `src/grass_mil/__init__.py`: GRASS-MIL: graph-based representation and discovery of phenotype-associated. +- `src/grass_mil/_compat.py`: Cross-version compatibility shims applied at package import. +- `src/grass_mil/_paths.py`: Project-root resolution shared by the CLI entrypoints. +- `src/grass_mil/batching.py`: Splitting batched graphs back into their per-subgraph parts. +- `src/grass_mil/callbacks.py`: Lightweight run-artefact callbacks. +- `src/grass_mil/contracts.py`: Cross-cutting contracts. +- `src/grass_mil/eval.py`: Module. +- `src/grass_mil/extract.py`: ``grass-mil-extract``: per-cell cache of trained models (see inference/cell_cache.py). +- `src/grass_mil/loocv.py`: Module. +- `src/grass_mil/nested_cv.py`: ``grass-mil-nested-cv``: nested, patient-grouped cross-validation (preprint protocol). +- `src/grass_mil/probe.py`: ``grass-mil-probe``: fit the linear probe head on a frozen encoder. +- `src/grass_mil/train.py`: Module. + +## Package: data pipeline + +- `src/grass_mil/data/__init__.py`: Package marker / re-exports. +- `src/grass_mil/data/components/__init__.py`: Package marker / re-exports. +- `src/grass_mil/data/components/datasets.py`: Module. +- `src/grass_mil/data/components/ego_radius.py`: Euclidean radius cutoff for k-hop ego-graphs. +- `src/grass_mil/data/components/feature_reducers.py`: Module. +- `src/grass_mil/data/components/graph_builders.py`: Module. +- `src/grass_mil/data/components/loaders.py`: Module. +- `src/grass_mil/data/components/merge.py`: Merge several graph units into one disconnected graph. +- `src/grass_mil/data/components/patching.py`: Module. +- `src/grass_mil/data/components/precompute.py`: Module. +- `src/grass_mil/data/components/samplers.py`: Module. +- `src/grass_mil/data/components/seed_sampling.py`: Interior-only seed sampling for ego-graph extraction. +- `src/grass_mil/data/components/spatial_types.py`: Module. +- `src/grass_mil/data/components/splits.py`: Explicit splits, fold files and patient-grouped inner folds. +- `src/grass_mil/data/components/transforms.py`: Module. +- `src/grass_mil/data/spatial_omics_datamodule.py`: Module. + +## Package: models + +- `src/grass_mil/models/__init__.py`: Package marker / re-exports. +- `src/grass_mil/models/bgrl_module.py`: Module. +- `src/grass_mil/models/components/__init__.py`: Package marker / re-exports. +- `src/grass_mil/models/components/attention.py`: Module. +- `src/grass_mil/models/components/backbones.py`: Module. +- `src/grass_mil/models/components/embeddings.py`: Input embeddings for node features. +- `src/grass_mil/models/components/factory.py`: Module. +- `src/grass_mil/models/components/heads.py`: Module. +- `src/grass_mil/models/components/losses.py`: Module. +- `src/grass_mil/models/components/pooling.py`: Module. +- `src/grass_mil/models/components/ssl.py`: Module. +- `src/grass_mil/models/supervised_module.py`: Module. +- `src/grass_mil/models/training/__init__.py`: Package marker / re-exports. +- `src/grass_mil/models/training/augmentations.py`: Graph augmentations for self-supervised pretraining. +- `src/grass_mil/models/training/bagging.py`: Module. +- `src/grass_mil/models/training/builders.py`: Module. +- `src/grass_mil/models/training/checkpoint_init.py`: Module. +- `src/grass_mil/models/training/class_weights.py`: Class weights for the region cross-entropy, computed from the TRAINING split only. +- `src/grass_mil/models/training/loss_utils.py`: Module. +- `src/grass_mil/models/training/optimization.py`: Module. +- `src/grass_mil/models/training/probe.py`: Linear probe pre-fit on a frozen encoder (preprint "probe head"). +- `src/grass_mil/models/training/ssl_runtime.py`: Module. + +## Package: inference + +- `src/grass_mil/inference/__init__.py`: Package marker / re-exports. +- `src/grass_mil/inference/aggregation.py`: Module. +- `src/grass_mil/inference/cell_cache.py`: Per-cell cache of a trained GRASS-MIL model: the input of niche discovery. +- `src/grass_mil/inference/collectors.py`: Module. +- `src/grass_mil/inference/interpretability_export.py`: Module. +- `src/grass_mil/inference/io.py`: Module. +- `src/grass_mil/inference/metrics.py`: Module. +- `src/grass_mil/inference/model_loading.py`: Build a supervised module from config and load trained weights outside ``Trainer``. +- `src/grass_mil/inference/predict.py`: Module. +- `src/grass_mil/inference/schemas.py`: Module. + +## Package: evaluation statistics + +- `src/grass_mil/evaluation/__init__.py`: Evaluation statistics for held-out scores (AUROC CIs, DeLong, operating points). +- `src/grass_mil/evaluation/cli.py`: ``grass-mil-scores``: AUROC CIs, DeLong comparisons and operating points. +- `src/grass_mil/evaluation/operating_points.py`: Sensitivity / specificity / PPV / NPV at pre-specified operating rules. +- `src/grass_mil/evaluation/stats.py`: AUROC statistics for held-out (out-of-fold) scores. + +## Package: interpretability (niches, metaclusters, report) + +- `src/grass_mil/interpretability/__init__.py`: Package marker / re-exports. +- `src/grass_mil/interpretability/core/__init__.py`: Package marker / re-exports. +- `src/grass_mil/interpretability/core/agreement.py`: Agreement between partitions of the same instances. +- `src/grass_mil/interpretability/core/attribution.py`: Exact attribution of a bag prediction to its instances. +- `src/grass_mil/interpretability/core/biomarkers.py`: Module. +- `src/grass_mil/interpretability/core/clustering.py`: Module. +- `src/grass_mil/interpretability/core/data.py`: Module. +- `src/grass_mil/interpretability/core/niches.py`: Niche identity: naming, the background label, and composition ordering. +- `src/grass_mil/interpretability/core/reduction.py`: Module. +- `src/grass_mil/interpretability/core/transfer.py`: Module. +- `src/grass_mil/interpretability/metaclusters/__init__.py`: Cross-fold niche replication: per-fold clustering, matching into metaclusters,. +- `src/grass_mil/interpretability/metaclusters/basis.py`: Stage 4: a shared, label-free k-means basis with per-model attribution. +- `src/grass_mil/interpretability/metaclusters/match.py`: Stage 2: match fold-clusters across folds into metaclusters. +- `src/grass_mil/interpretability/metaclusters/records.py`: Stage 1: cluster every fold model's held-out cells in its own space. +- `src/grass_mil/interpretability/metaclusters/transfer.py`: Stage 3: transfer every fold model's clustering to ALL regions in its own space. +- `src/grass_mil/interpretability/metaclusters_cli.py`: ``grass-mil-metaclusters``: cross-fold niche replication (cluster / match / transfer / basis). +- `src/grass_mil/interpretability/niches/__init__.py`: Niche discovery on a trained GRASS-MIL model's per-cell cache. +- `src/grass_mil/interpretability/niches/colocalisation.py`: Niche-niche co-localisation on the cell graph: log2(observed / expected) edge counts. +- `src/grass_mil/interpretability/niches/figures.py`: Figures of the niche report (matplotlib; UMAP optional). +- `src/grass_mil/interpretability/niches/margins.py`: Per-cell attention-weighted margins and the attention feature space. +- `src/grass_mil/interpretability/niches/pipeline.py`: Niche discovery on one model's cell cache. +- `src/grass_mil/interpretability/niches/tables.py`: Per-niche summary table (the preprint's cluster table). +- `src/grass_mil/interpretability/niches_cli.py`: ``grass-mil-niches``: niche discovery on one model's cell cache. +- `src/grass_mil/interpretability/pipeline.py`: Module. +- `src/grass_mil/interpretability/plugins/__init__.py`: Package marker / re-exports. +- `src/grass_mil/interpretability/plugins/base.py`: Module. +- `src/grass_mil/interpretability/plugins/builtin.py`: Module. +- `src/grass_mil/interpretability/plugins/registry.py`: Module. +- `src/grass_mil/interpretability/report_cli.py`: Module. +- `src/grass_mil/interpretability/reporting/__init__.py`: Package marker / re-exports. +- `src/grass_mil/interpretability/reporting/html.py`: Module. +- `src/grass_mil/interpretability/reporting/pdf.py`: Module. +- `src/grass_mil/interpretability/reporting/plotly_builders.py`: Module. +- `src/grass_mil/interpretability/reporting/render.py`: Module. +- `src/grass_mil/interpretability/reporting/sections.py`: Module. +- `src/grass_mil/interpretability/reporting/snapshot.py`: Module. +- `src/grass_mil/interpretability/tier2/__init__.py`: Package marker / re-exports. +- `src/grass_mil/interpretability/tier2/autocorrelation.py`: Global spatial autocorrelation (Moran's I). +- `src/grass_mil/interpretability/tier2/cell_level.py`: Cell-level tier-2 analyses, run within each niche. +- `src/grass_mil/interpretability/tier2/filtration.py`: Module. +- `src/grass_mil/interpretability/tier2/neighborhood.py`: Module. +- `src/grass_mil/interpretability/tier2/ripley.py`: Ripley's K/L and the bivariate cross-K. +- `src/grass_mil/interpretability/tier2/tissue_graph.py`: Module. + +## Package: classical spatial statistics + +- `src/grass_mil/spatial_stats/__init__.py`: Classical spatial statistics on the cell graph, per region, compared between groups. +- `src/grass_mil/spatial_stats/cellular_neighbourhoods.py`: Cellular neighbourhoods: k-NN window composition clustered with k-means. +- `src/grass_mil/spatial_stats/cli.py`: ``grass-mil-spatial-stats``: classical spatial statistics on a cell cache. +- `src/grass_mil/spatial_stats/contacts.py`: Contact fractions on the cell graph. +- `src/grass_mil/spatial_stats/cross_k.py`: Ripley cross-K between a reference group and partner groups, normalised by pi r^2. +- `src/grass_mil/spatial_stats/nhood_enrichment.py`: Neighbourhood enrichment z-scores on a cell graph (label permutation null). +- `src/grass_mil/spatial_stats/run.py`: Run the classical spatial statistics over a cell cache and compare groups. +- `src/grass_mil/spatial_stats/tests.py`: Per-region -> per-patient summaries and two-group tests with BH correction. + +## Package: utilities + +- `src/grass_mil/utils/__init__.py`: Package marker / re-exports. +- `src/grass_mil/utils/instantiators.py`: Module. +- `src/grass_mil/utils/logging_utils.py`: Module. +- `src/grass_mil/utils/pylogger.py`: Module. +- `src/grass_mil/utils/rich_utils.py`: Module. +- `src/grass_mil/utils/utils.py`: Module. + +## Hydra configs + +- `src/grass_mil/configs/__init__.py`: Package marker / re-exports. +- `src/grass_mil/configs/augmentation/bgrl_paper.yaml`: Degree-importance-weighted node/edge dropping plus Gaussian noise on the. +- `src/grass_mil/configs/augmentation/uniform.yaml`: Uniform edge dropping and feature masking, independent of graph structure. +- `src/grass_mil/configs/callbacks/default.yaml`: Hydra config: `default`. +- `src/grass_mil/configs/callbacks/early_stopping.yaml`: https://lightning.ai/docs/pytorch/stable/api/lightning.pytorch.callbacks.EarlyStopping.html. +- `src/grass_mil/configs/callbacks/mil_selection.yaml`: Model selection for MIL fine-tuning (preprint): keep the epoch with the best. +- `src/grass_mil/configs/callbacks/model_checkpoint.yaml`: https://lightning.ai/docs/pytorch/stable/api/lightning.pytorch.callbacks.ModelCheckpoint.html. +- `src/grass_mil/configs/callbacks/model_summary.yaml`: https://lightning.ai/docs/pytorch/stable/api/lightning.pytorch.callbacks.RichModelSummary.html. +- `src/grass_mil/configs/callbacks/none.yaml`: Hydra config: `none`. +- `src/grass_mil/configs/callbacks/pretrain_checkpoint.yaml`: Pretraining: a checkpoint every N optimizer steps (preprint: 5000), all kept,. +- `src/grass_mil/configs/callbacks/refit.yaml`: Refit on all development folds for a fixed number of epochs: no early. +- `src/grass_mil/configs/callbacks/rich_progress_bar.yaml`: https://lightning.ai/docs/pytorch/latest/api/lightning.pytorch.callbacks.RichProgressBar.html. +- `src/grass_mil/configs/data/sampler/ego_graph_mil.yaml`: Ego-graph bags for MIL fine-tuning (preprint defaults). +- `src/grass_mil/configs/data/sampler/ego_graph_pretrain.yaml`: Ego-graph batches for BGRL pretraining (preprint "A3" defaults): roots drawn. +- `src/grass_mil/configs/data/sampler/identity.yaml`: Hydra config: `identity`. +- `src/grass_mil/configs/data/sampler/shadow_custom.yaml`: Hydra config: `shadow_custom`. +- `src/grass_mil/configs/data/sampler/shadow_native.yaml`: Hydra config: `shadow_native`. +- `src/grass_mil/configs/data/spatial_omics.yaml`: Hydra config: `spatial_omics`. +- `src/grass_mil/configs/debug/default.yaml`: default debugging setup, runs 1 full epoch. +- `src/grass_mil/configs/debug/fdr.yaml`: runs 1 train, 1 validation and 1 test step. +- `src/grass_mil/configs/debug/limit.yaml`: uses only 1% of the training data and 5% of validation/test data. +- `src/grass_mil/configs/debug/overfit.yaml`: overfits to 3 batches. +- `src/grass_mil/configs/debug/profiler.yaml`: runs with execution time profiling. +- `src/grass_mil/configs/eval.yaml`: Hydra config: `eval`. +- `src/grass_mil/configs/experiment/example_cohort.yaml`: Template for a cohort of whole regions (cores, ROIs, slides) given as CSV cell. +- `src/grass_mil/configs/experiment/grass_mil_pretrain_a3.yaml`: Same recipe as `regime=pretrain_a3`, for runs without a cohort experiment file: +- `src/grass_mil/configs/extract.yaml`: grass-mil-extract: per-cell cache (embeddings, instance logits, attention) of. +- `src/grass_mil/configs/extras/default.yaml`: disable python warnings if they annoy you. +- `src/grass_mil/configs/hydra/default.yaml`: https://hydra.cc/docs/configure_hydra/intro/. +- `src/grass_mil/configs/inference/aggregation/default.yaml`: Hydra config: `default`. +- `src/grass_mil/configs/inference/embeddings/default.yaml`: Hydra config: `default`. +- `src/grass_mil/configs/inference/interpretability/default.yaml`: Hydra config: `default`. +- `src/grass_mil/configs/inference/metrics/default.yaml`: Hydra config: `default`. +- `src/grass_mil/configs/inference/predict.yaml`: Hydra config: `predict`. +- `src/grass_mil/configs/interpretability/clustering/agglomerative.yaml`: Hydra config: `agglomerative`. +- `src/grass_mil/configs/interpretability/clustering/dbscan.yaml`: Hydra config: `dbscan`. +- `src/grass_mil/configs/interpretability/clustering/hdbscan.yaml`: Hydra config: `hdbscan`. +- `src/grass_mil/configs/interpretability/clustering/kmeans.yaml`: Hydra config: `kmeans`. +- `src/grass_mil/configs/interpretability/clustering/optics.yaml`: Hydra config: `optics`. +- `src/grass_mil/configs/interpretability/clustering/spectral.yaml`: Hydra config: `spectral`. +- `src/grass_mil/configs/interpretability/plugins/default.yaml`: Hydra config: `default`. +- `src/grass_mil/configs/interpretability/plugins/full.yaml`: Every builtin analysis enabled. +- `src/grass_mil/configs/interpretability/reduction/pca.yaml`: Hydra config: `pca`. +- `src/grass_mil/configs/interpretability/reduction/tsne.yaml`: Hydra config: `tsne`. +- `src/grass_mil/configs/interpretability/reduction/umap.yaml`: Hydra config: `umap`. +- `src/grass_mil/configs/interpretability/report.yaml`: Hydra config: `report`. +- `src/grass_mil/configs/local/.gitkeep`: Placeholder for untracked machine-local configs. +- `src/grass_mil/configs/logger/aim.yaml`: https://aimstack.io/. +- `src/grass_mil/configs/logger/comet.yaml`: https://www.comet.ml. +- `src/grass_mil/configs/logger/csv.yaml`: csv logger built in lightning. +- `src/grass_mil/configs/logger/many_loggers.yaml`: train with many loggers at once. +- `src/grass_mil/configs/logger/mlflow.yaml`: https://mlflow.org. +- `src/grass_mil/configs/logger/neptune.yaml`: https://neptune.ai. +- `src/grass_mil/configs/logger/tensorboard.yaml`: https://www.tensorflow.org/tensorboard/. +- `src/grass_mil/configs/logger/wandb.yaml`: https://wandb.ai. +- `src/grass_mil/configs/loocv.yaml`: Hydra config: `loocv`. +- `src/grass_mil/configs/metaclusters.yaml`: grass-mil-metaclusters: cross-fold niche replication. +- `src/grass_mil/configs/model/attention/gated.yaml`: Plain gated attention (Ilse et al. 2018), one attention channel per class. +- `src/grass_mil/configs/model/attention/gated_projected.yaml`: Projected gated attention (GRASS-MIL default): Linear + LeakyReLU projection,. +- `src/grass_mil/configs/model/bgrl_a3.yaml`: Hydra config: `bgrl_a3`. +- `src/grass_mil/configs/model/bgrl_module.yaml`: Hydra config: `bgrl_module`. +- `src/grass_mil/configs/model/encoder/gat.yaml`: Hydra config: `gat`. +- `src/grass_mil/configs/model/encoder/gcn.yaml`: Hydra config: `gcn`. +- `src/grass_mil/configs/model/encoder/gin.yaml`: Small GIN encoder without edge features: a lightweight default for smoke runs. +- `src/grass_mil/configs/model/encoder/gine.yaml`: Hydra config: `gine`. +- `src/grass_mil/configs/model/encoder/gine_length.yaml`: GRASS-MIL encoder (preprint defaults): 4 GINE layers of width 256 conditioned. +- `src/grass_mil/configs/model/encoder/graphsage.yaml`: Hydra config: `graphsage`. +- `src/grass_mil/configs/model/grass_mil.yaml`: Hydra config: `grass_mil`. +- `src/grass_mil/configs/model/supervised_module.yaml`: Hydra config: `supervised_module`. +- `src/grass_mil/configs/nested_cv.yaml`: grass-mil-nested-cv: the preprint's nested, patient-grouped cross-validation. +- `src/grass_mil/configs/niches.yaml`: grass-mil-niches: niche discovery on a trained model's cell cache. +- `src/grass_mil/configs/optim/adamw.yaml`: Hydra config: `adamw`. +- `src/grass_mil/configs/paths/default.yaml`: path to root directory. +- `src/grass_mil/configs/probe.yaml`: grass-mil-probe: fit the linear probe head on a frozen encoder. +- `src/grass_mil/configs/regime/linear_ftall.yaml`: Preprint "ftALL": encoder fine-tuned end to end (lr x1.0), linear head. +- `src/grass_mil/configs/regime/linear_ftall_reg.yaml`: Preprint "ftALL, lr x0.3 + regularisation" (the STAS model of record): +- `src/grass_mil/configs/regime/linear_probehead.yaml`: Preprint "probehead": frozen encoder, LINEAR head pre-fitted by grass-mil-probe. +- `src/grass_mil/configs/regime/mil.yaml`: GRASS-MIL default regime: BGRL-initialised encoder fine-tuned end to end,. +- `src/grass_mil/configs/regime/mil_frozen.yaml`: Frozen encoder (learning-rate scale 0, kept in eval mode); MLP head and. +- `src/grass_mil/configs/regime/pretrain_a3.yaml`: GRASS-MIL encoder pretraining (preprint "A3"): BGRL on the ego-graphs of. +- `src/grass_mil/configs/scheduler/cosine_epoch.yaml`: Hydra config: `cosine_epoch`. +- `src/grass_mil/configs/scheduler/cosine_mil_step.yaml`: Cosine annealing per optimizer step over the whole run (preprint): with. +- `src/grass_mil/configs/scheduler/cosine_step.yaml`: Hydra config: `cosine_step`. +- `src/grass_mil/configs/scores.yaml`: grass-mil-scores: evaluation statistics for held-out scores. +- `src/grass_mil/configs/spatial_stats.yaml`: grass-mil-spatial-stats: classical spatial statistics per region, compared. +- `src/grass_mil/configs/task/finetune_mean.yaml`: Hydra config: `finetune_mean`. +- `src/grass_mil/configs/task/finetune_mil.yaml`: Hydra config: `finetune_mil`. +- `src/grass_mil/configs/task/finetune_survival.yaml`: Cox survival with MIL attention pooling. The head emits a single scalar. +- `src/grass_mil/configs/task/pretrain_bgrl.yaml`: Hydra config: `pretrain_bgrl`. +- `src/grass_mil/configs/task/pretrain_bgrl_a3.yaml`: BGRL pretraining, preprint "A3" recipe: AdamW 5e-5 / weight decay 1e-5,. +- `src/grass_mil/configs/train.yaml`: specify here default configuration. +- `src/grass_mil/configs/trainer/cpu.yaml`: Hydra config: `cpu`. +- `src/grass_mil/configs/trainer/ddp.yaml`: Hydra config: `ddp`. +- `src/grass_mil/configs/trainer/ddp_sim.yaml`: Hydra config: `ddp_sim`. +- `src/grass_mil/configs/trainer/default.yaml`: Hydra config: `default`. +- `src/grass_mil/configs/trainer/gpu.yaml`: Hydra config: `gpu`. +- `src/grass_mil/configs/trainer/gpu_amp.yaml`: Hydra config: `gpu_amp`. +- `src/grass_mil/configs/trainer/mps.yaml`: Hydra config: `mps`. + +## Documentation + +- `docs/capabilities_and_modes.md`: Capabilities And Modes. +- `docs/configuration_guide.md`: Configuration Guide. +- `docs/dataset_preparation.md`: Dataset Preparation Guide. +- `docs/evaluation_stats.md`: Evaluation Statistics (`grass-mil-scores`). +- `docs/getting_started.md`: Getting Started: Typical Usage On Your Own Cohort. +- `docs/hyperparameter_reference.md`: Hyperparameter Reference. +- `docs/inference_utility_suite.md`: Inference Utility Suite. +- `docs/interpretability_extension_guide.md`: Interpretability Extension Guide. +- `docs/interpretability_suite.md`: Interpretability Suite. +- `docs/method_correspondence.md`: Method Correspondence With The Preprint. +- `docs/model_component_contracts.md`: Model Component Contracts. +- `docs/nested_cv.md`: Nested Cross-Validation (the preprint protocol). +- `docs/niche_discovery.md`: Niche Discovery And Cross-Fold Replication. +- `docs/repository_map.md`: Repository Map. +- `docs/spatial_statistics.md`: Classical Spatial Statistics (`grass-mil-spatial-stats`). +- `docs/workflows_train_infer.md`: Training And Inference Workflows. + +## Vignettes + +- `vignettes/01_anndata_to_spatial_graphs.ipynb`: Vignette 1: AnnData -> manifest -> cellular graphs. +- `vignettes/02_pretraining_and_niches.ipynb`: Vignette 2: BGRL pretraining, fine-tuning, instance export, interpretability report. +- `vignettes/03_nested_cv_niches_and_metaclusters.ipynb`: Vignette 3: nested CV, cell extraction, niches, metaclusters, scores, spatial statistics. +- `vignettes/README.md`: Index of the worked examples on the public MIBI-TOF dataset. + +## Tests + +- `tests/__init__.py`: Package marker / re-exports. +- `tests/configs/test_component_instantiation.py`: Compose the model/encoder and model/attention groups through Hydra and build them. +- `tests/configs/test_task_preset_composition.py`: Compose task presets through Hydra the way the CLI does. +- `tests/conftest.py`: Shared pytest fixtures (Hydra configs on a synthetic datamodule). +- `tests/contracts/test_model_component_contracts.py`: Tests for model component contracts. +- `tests/data/test_datamodule_sampler_exposure.py`: Tests for datamodule sampler exposure. +- `tests/data/test_loader_flags.py`: Tests for loader flags. +- `tests/data/test_precompute_manifest_regions.py`: Tests for precompute manifest regions. +- `tests/data/test_precompute_reducer_modes.py`: Tests for precompute reducer modes. +- `tests/data/test_samplers.py`: Tests for samplers. +- `tests/data/test_seed_sampling.py`: Contracts for interior seed restriction and the ego-radius cutoff. +- `tests/eval/test_aggregation.py`: Tests for aggregation. +- `tests/eval/test_collectors.py`: Tests for collectors. +- `tests/eval/test_embeddings.py`: Tests for embeddings. +- `tests/eval/test_interpretability_export.py`: Tests for interpretability export. +- `tests/eval/test_io.py`: Tests for io. +- `tests/eval/test_metrics.py`: Tests for metrics. +- `tests/eval/test_predict_cli.py`: Tests for predict cli. +- `tests/helpers/__init__.py`: Package marker / re-exports. +- `tests/helpers/config_paths.py`: Locate the packaged Hydra config tree. +- `tests/helpers/package_available.py`: Tests for package available. +- `tests/helpers/run_if.py`: Adapted from: +- `tests/helpers/run_sh_command.py`: Tests for run sh command. +- `tests/helpers/synthetic_cohort.py`: Synthetic cohort on disk (regions, labels, folds) with a planted niche, used by the phase tests. +- `tests/helpers/synthetic_datamodule.py`: In-memory bag datamodule for fast training tests. +- `tests/interpretability/test_attribution.py`: Contracts for the exact additive attribution. +- `tests/interpretability/test_cell_level.py`: Cell-level tier-2 analyses. +- `tests/interpretability/test_core.py`: Tests for core. +- `tests/interpretability/test_fidelity_phase6.py`: Phase 6: niche discovery (cosine HDBSCAN, relattn margins, tables, co-localisation, pipeline). +- `tests/interpretability/test_fidelity_phase7.py`: Phase 7: cross-fold metaclusters, transfer and shared basis. +- `tests/interpretability/test_niches.py`: Niche naming, the Background label, and composition ordering. +- `tests/interpretability/test_pipeline_and_report.py`: Tests for pipeline and report. +- `tests/interpretability/test_plugins_and_tier2.py`: Tests for plugins and tier2. +- `tests/interpretability/test_spatial_statistics.py`: Contracts for Moran's I, Ripley's cross-L, and cross-space agreement. +- `tests/interpretability/test_tissue_graph.py`: Tests for tissue graph. +- `tests/interpretability/test_transfer.py`: Tests for transfer. +- `tests/models/test_attention.py`: Tests for attention. +- `tests/models/test_augmentations.py`: Contracts for the BGRL view generators. +- `tests/models/test_backbones.py`: Tests for backbones. +- `tests/models/test_bagging.py`: Bag-pooling contracts, including the attribution identity. +- `tests/models/test_fidelity_phase1.py`: Phase 1 fidelity: graph edge features, encoder epsilon/init, attention init and space. +- `tests/models/test_fidelity_phase2.py`: Phase 2 fidelity: region bags, root sampling, parameter groups, schedules, class weights,. +- `tests/models/test_fidelity_phase3.py`: Phase 3: explicit splits, probe pre-fit, nested cross-validation. +- `tests/models/test_fidelity_phase4.py`: Phase 4: BGRL "A3" pretraining fidelity (merged-unit sampling, momentum schedule,. +- `tests/models/test_fidelity_phase5.py`: Phase 5: cell cache extraction, patient aggregation, evaluation statistics. +- `tests/models/test_losses.py`: Tests for losses. +- `tests/models/test_node_embeddings.py`: Contracts for the encoder's input embeddings. +- `tests/models/test_optimization.py`: Tests for optimization. +- `tests/models/test_runtime.py`: Tests for runtime. +- `tests/models/test_ssl.py`: Tests for ssl. +- `tests/spatial_stats/__init__.py`: Package marker / re-exports. +- `tests/spatial_stats/test_fidelity_phase8.py`: Phase 8: classical spatial statistics. +- `tests/test_configs.py`: Tests for configs. +- `tests/test_e2e_synthetic.py`: End-to-end smoke test of the public analysis chain on the synthetic cohort. +- `tests/test_eval.py`: Tests for eval. +- `tests/test_loocv.py`: Tests for loocv. +- `tests/test_packaging.py`: Guards on the installable-package contract. +- `tests/test_spatial_omics_datamodule.py`: Tests for spatial omics datamodule. +- `tests/test_train.py`: Tests for train. +- `tests/test_train_regimes.py`: Tests for train regimes. + +## Other + +- `data/.gitkeep`: Tracked file. +- `logs/.gitkeep`: Tracked file. + +## Generated at runtime (untracked) + +- `logs/`: Hydra run directories (checkpoints, `metrics_per_epoch.jsonl`, predictions, caches, reports). +- `data/`: raw manifests and processed graphs (`processed_index.json`, `metadata.json`, `graphs/`). +- `vignettes/vignette_work/`: data, graphs and runs produced by the vignettes. diff --git a/docs/spatial_statistics.md b/docs/spatial_statistics.md new file mode 100644 index 0000000..3b9a706 --- /dev/null +++ b/docs/spatial_statistics.md @@ -0,0 +1,25 @@ +# Classical Spatial Statistics (`grass-mil-spatial-stats`) + +Model-free statistics on the cell graph of every region, averaged per patient +and compared between classes with Mann-Whitney U and Benjamini-Hochberg +correction within each analysis. They run on any cell cache written by +`grass-mil-extract` (only cell types, positions and the graph are used). + +```bash +grass-mil-spatial-stats cells_dir=runs/cells/fold0 output_dir=runs/spatial \ + labels.label_file=data/raw/labels.csv labels.label_column=STAS labels.patient_column=PATIENT \ + reference=[tumor] partners=[myeloid,CD8,Treg] infiltrating=[myeloid,CD8,Treg] \ + group_map='{normal: tumor, hypoxic: tumor, cd8: CD8, ki67_cd8: CD8}' +``` + +| analysis | per region | patient level | +|---|---|---| +| `nhood` | neighbourhood enrichment z-scores of every group pair on the cell graph, `nhood.n_perms` label permutations (`nhood.backend=squidpy` delegates to `squidpy.gr.nhood_enrichment` on the same graph) | mean z per pair, test | +| `cn` | `cn.window_k`-nearest-neighbour window composition, `MiniBatchKMeans(cn.n_cn)` fitted on `cn.sample_size` windows of all regions, share of each cellular neighbourhood | mean share, CLR-transformed, test; `cn_centres.csv` gives the CN compositions | +| `contacts` | fraction of `reference` cells touching >= 1 cell of each partner group, homotypic contact, fraction of each `infiltrating` group whose neighbours are mostly reference cells | mean, test | +| `crossk` | Ripley cross-K reference -> partner at `crossk.radii`, normalised by pi r^2 (1 = complete spatial randomness; self pairs removed for reference -> reference) | mean, test | + +Outputs: `*_per_region.csv`, `nhood_tests.csv`, `cn_tests.csv`, `contact_tests.csv`, +`crossk_tests.csv`, `report_spatial.md`, `spatial_stats_summary.json`. +`group_map` collapses fine cell types into coarse groups before everything +(identity by default). Distances are in the cache's coordinate units. diff --git a/docs/workflows_train_infer.md b/docs/workflows_train_infer.md index d184764..7f90c65 100644 --- a/docs/workflows_train_infer.md +++ b/docs/workflows_train_infer.md @@ -2,6 +2,11 @@ This guide provides flow-through instructions for all major `grass-mil` workflows. +> New here? The GRASS-MIL path (pretraining, regimes, nested CV, niches) is +> walked through in [getting_started.md](getting_started.md), and the +> configuration system in [configuration_guide.md](configuration_guide.md). +> This page covers the generic training and inference machinery underneath. + Interpretability workflows are documented in: - `docs/interpretability_suite.md` @@ -264,6 +269,21 @@ Training artifacts (`paths.output_dir`): ### Step 1: Run SSL Pretraining (BGRL) +The GRASS-MIL recipe ("A3" in the preprint) is one experiment preset: + +```bash +grass-mil-train experiment=my_cohort regime=pretrain_a3 paths.output_dir=runs/pretrain_a3 trainer=gpu +# or, without a cohort file: grass-mil-train experiment=grass_mil_pretrain_a3 data.raw_manifest_path=... data.processed_dir=... +``` + +It composes `model=bgrl_a3`, `task=pretrain_bgrl_a3`, `callbacks=pretrain_checkpoint` +and `data/sampler=ego_graph_pretrain` (see +[`hyperparameter_reference.md`](hyperparameter_reference.md) section 3.4); every +value is overridable, e.g. `task.total_steps=50000 model.encoder.num_layers=3 +data.sampler.runtime.depth=3 task.augmentation.feature_noise_columns=[]`. + +The generic pretraining task remains available: + ```bash grass-mil-train task=pretrain_bgrl model=bgrl_module data=spatial_omics ``` @@ -277,7 +297,7 @@ The pretraining task also sets: - View generation, selected via the `augmentation` config group (`bgrl_paper` or `uniform`) — see the augmentation section of [`hyperparameter_reference.md`](hyperparameter_reference.md) -- Momentum schedule (`momentum`, `momentum_min`, `warmup_steps`, `total_steps`) +- Momentum schedule (`momentum`, `momentum_final`, `momentum_warmup_steps`, `total_steps`) ### Step 2: Choose A Checkpoint @@ -316,13 +336,43 @@ Mapping behavior (`encoder_init_map`): - `auto_bgrl_or_identity`: remaps known BGRL encoder prefixes such as `online_encoder.*` and `ssl_model.online_encoder.*` to `encoder.*` - `identity`: no key remapping +- `spacegm_bgrl`: imports encoders pretrained with the original SPACE-GM style BGRL code (`x_embedding`, `feat_embedding`, `gnns.{i}.gine.*`, `batch_norms.{i}`) Common failures: 1. `FileNotFoundError` if `model.init_from_ckpt` path is invalid. 2. Missing/unexpected key mismatch with `model.init_strict=true`. 3. Shape mismatch when encoder dimensions differ between pretrain and fine-tune configs. -4. Invalid `encoder_init_map` value (must be `identity` or `auto_bgrl_or_identity`). +4. Invalid `encoder_init_map` value (must be `identity`, `auto_bgrl_or_identity` or `spacegm_bgrl`). + +## 2b) GRASS-MIL Regimes, Probe And Nested Cross-Validation + +The preprint's fine-tuning settings are `regime=` presets on top of +`model=grass_mil task=finetune_mil` (one region = one bag, region-level +selection); see [`nested_cv.md`](nested_cv.md) for the full protocol and +[`hyperparameter_reference.md`](hyperparameter_reference.md) section 12a for the +values. + +```bash +# one run with explicit splits (ids = region ids of the processed cohort) +grass-mil-train regime=mil data=... model.init_from_ckpt=runs/pretrain_a3/checkpoints/last.ckpt \ + data.split.explicit.train=train.json data.split.explicit.val=val.json data.split.explicit.test=test.json + +# linear head pre-fitted by a logistic probe on the frozen encoder, then attention only +grass-mil-probe regime=linear_probehead data=... model.init_from_ckpt=.ckpt \ + data.split.explicit.train=train.json data.split.explicit.val=val.json probe.output_path=probe_head.pt +grass-mil-train regime=linear_probehead data=... model.init_from_ckpt=.ckpt \ + task.head_init_path=probe_head.pt data.split.explicit.train=train.json ... + +# the whole nested CV (fits the probes itself) +grass-mil-nested-cv regime=linear_ftall_reg data=... model.init_from_ckpt=.ckpt \ + nested_cv.folds_file=folds.json data.graph_labels.patient_column=PATIENT +``` + +Downstream of a trained model: `grass-mil-extract` (cells), `grass-mil-niches`, +`grass-mil-metaclusters` ([`niche_discovery.md`](niche_discovery.md)), +`grass-mil-scores` ([`evaluation_stats.md`](evaluation_stats.md)), +`grass-mil-spatial-stats` ([`spatial_statistics.md`](spatial_statistics.md)). ## 3) Adjusting Model Settings (Aggregation, Backbone, Loss, MIL Options) diff --git a/environment.yaml b/environment.yaml index d73d90f..9ddd717 100644 --- a/environment.yaml +++ b/environment.yaml @@ -61,6 +61,8 @@ dependencies: - hydra-colorlog - rootutils - umap-learn + - matplotlib + - statsmodels # optional: required only for SingleCellExperiment/rds loading, and also # needs an R runtime with Bioconductor # - rpy2 diff --git a/pyproject.toml b/pyproject.toml index 6ba27a4..a4a1e31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,8 @@ build-backend = "hatchling.build" [project] name = "grass-mil" -version = "0.1.0" -description = "Geometric deep learning framework for spatial omics data" +version = "1.0.0" +description = "GRASS-MIL: graph-based representation and discovery of phenotype-associated spatial structures with multiple instance learning" readme = "README.md" requires-python = ">=3.11" license = { file = "LICENSE" } @@ -17,7 +17,7 @@ dependencies = [ "lightning-utilities>=0.10", "torchmetrics>=0.11.4", "torch-geometric>=2.5", - "hydra-core==1.3.2", + "hydra-core>=1.3.5,<1.4", "hydra-colorlog==1.2.0", "omegaconf", "numpy", @@ -32,7 +32,9 @@ dependencies = [ ] [project.optional-dependencies] -interpretability = ["plotly", "umap-learn", "jinja2"] +interpretability = ["plotly", "umap-learn", "jinja2", "matplotlib"] +stats = ["statsmodels"] +plots = ["matplotlib", "umap-learn"] report = ["kaleido", "playwright"] spatial-stats = ["squidpy", "scanpy", "anndata"] io = ["anndata", "shapely"] @@ -44,6 +46,13 @@ sce = ["rpy2"] grass-mil-train = "grass_mil.train:main" grass-mil-eval = "grass_mil.eval:main" grass-mil-loocv = "grass_mil.loocv:main" +grass-mil-probe = "grass_mil.probe:main" +grass-mil-nested-cv = "grass_mil.nested_cv:main" +grass-mil-extract = "grass_mil.extract:main" +grass-mil-scores = "grass_mil.evaluation.cli:main" +grass-mil-niches = "grass_mil.interpretability.niches_cli:main" +grass-mil-metaclusters = "grass_mil.interpretability.metaclusters_cli:main" +grass-mil-spatial-stats = "grass_mil.spatial_stats.cli:main" grass-mil-predict = "grass_mil.inference.predict:main" grass-mil-report = "grass_mil.interpretability.report_cli:main" @@ -63,6 +72,9 @@ filterwarnings = [ "ignore::UserWarning", ] log_cli = "True" +markers = [ + "slow: end-to-end chains that take more than a minute (deselect with -m 'not slow')", +] minversion = "6.0" testpaths = "tests/" diff --git a/scripts/schedule.sh b/scripts/schedule.sh deleted file mode 100644 index 3af09df..0000000 --- a/scripts/schedule.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -# Schedule execution of many runs -# Run from the repository root with: bash scripts/schedule.sh - -grass-mil-train trainer.max_epochs=5 logger=csv - -grass-mil-train trainer.max_epochs=10 logger=csv diff --git a/src/grass_mil/__init__.py b/src/grass_mil/__init__.py index 56ad939..650186b 100644 --- a/src/grass_mil/__init__.py +++ b/src/grass_mil/__init__.py @@ -1,5 +1,17 @@ +"""GRASS-MIL: graph-based representation and discovery of phenotype-associated +spatial structures with multiple instance learning.""" + +from importlib.metadata import PackageNotFoundError, version + from grass_mil._compat import allow_omegaconf_in_checkpoints # Applied at import so every entrypoint and any user code that loads a # checkpoint gets it. See _compat for why this is needed under torch>=2.6. allow_omegaconf_in_checkpoints() + +try: + __version__ = version("grass-mil") +except PackageNotFoundError: # source checkout without an installed distribution + __version__ = "0.0.0" + +__all__ = ["__version__"] diff --git a/src/grass_mil/callbacks.py b/src/grass_mil/callbacks.py new file mode 100644 index 0000000..430f9e9 --- /dev/null +++ b/src/grass_mil/callbacks.py @@ -0,0 +1,110 @@ +"""Lightweight run-artefact callbacks. + +``EpochMetricsWriter`` appends one JSON line per epoch with every logged +``val/*`` and ``train/*`` metric to ``metrics_per_epoch.jsonl`` in the run's +output directory. The nested cross-validation driver reads it to pick the best +epoch (``argmax val/auroc``) of each inner fold. +""" + +from __future__ import annotations + +import json +import math +from pathlib import Path +from typing import Any, Dict, Optional + +import lightning as L +import torch + +__all__ = ["EpochMetricsWriter", "write_test_predictions"] + + +def _to_float(value: Any) -> Optional[float]: + try: + if isinstance(value, torch.Tensor): + value = value.detach().float().cpu().item() + value = float(value) + except (TypeError, ValueError): + return None + if math.isnan(value): + return None + return value + + +class EpochMetricsWriter(L.Callback): + def __init__(self, output_dir: str, filename: str = "metrics_per_epoch.jsonl") -> None: + super().__init__() + self.output_dir = Path(output_dir) + self.filename = filename + self._path: Optional[Path] = None + + @property + def path(self) -> Path: + if self._path is None: + self.output_dir.mkdir(parents=True, exist_ok=True) + self._path = self.output_dir / self.filename + return self._path + + def on_train_start(self, trainer: L.Trainer, pl_module: L.LightningModule) -> None: + if trainer.is_global_zero and self.path.exists(): + self.path.unlink() + + def on_validation_end(self, trainer: L.Trainer, pl_module: L.LightningModule) -> None: + # ``on_validation_end`` runs after the module's ``on_validation_epoch_end`` + # (where the region-level metrics are logged), unlike the callback's own + # ``on_validation_epoch_end``, which would see the previous epoch's values. + if trainer.sanity_checking or not trainer.is_global_zero: + return + row: Dict[str, Any] = { + "epoch": int(trainer.current_epoch), + "step": int(trainer.global_step), + } + for key, value in trainer.callback_metrics.items(): + if key.startswith(("val/", "train/")) and not key.endswith("_step"): + v = _to_float(value) + if v is not None: + row[key] = v + with self.path.open("a") as fh: + fh.write(json.dumps(row) + "\n") + + +def write_test_predictions(pl_module: L.LightningModule, output_dir: str) -> Optional[Path]: + """Write region-level test predictions (``test_predictions.csv``) if present.""" + preds = getattr(pl_module, "test_predictions", None) + if not preds: + return None + import pandas as pd + + logits = preds["logits"] + targets = preds["targets"] + rows = [] + n = int(logits.shape[0]) + region_ids = preds.get("region_ids") or [None] * n + sample_ids = preds.get("sample_ids") or [None] * n + for i in range(n): + row: Dict[str, Any] = { + "bag_id": preds["bag_ids"][i], + "region_id": region_ids[i] if i < len(region_ids) else None, + "sample_id": sample_ids[i] if i < len(sample_ids) else None, + } + lg = logits[i].view(-1) + for c in range(int(lg.numel())): + row[f"logit_{c}"] = float(lg[c]) + if preds.get("target_type") == "categorical": + probs = torch.softmax(lg.float(), dim=0) + for c in range(int(lg.numel())): + row[f"prob_class_{c}"] = float(probs[c]) + row["label"] = int(targets[i].view(-1)[0]) + elif preds.get("target_type") == "binary": + row["prob_class_1"] = float(torch.sigmoid(lg.float())[0]) + row["label"] = int(targets[i].view(-1)[0]) + else: + tv = targets[i].view(-1) + for c in range(int(tv.numel())): + row[f"target_{c}"] = float(tv[c]) + rows.append(row) + out = Path(output_dir) + out.mkdir(parents=True, exist_ok=True) + path = out / "test_predictions.csv" + pd.DataFrame(rows).to_csv(path, index=False) + return path diff --git a/src/grass_mil/configs/callbacks/mil_selection.yaml b/src/grass_mil/configs/callbacks/mil_selection.yaml new file mode 100644 index 0000000..661a338 --- /dev/null +++ b/src/grass_mil/configs/callbacks/mil_selection.yaml @@ -0,0 +1,27 @@ +# Model selection for MIL fine-tuning (preprint): keep the epoch with the best +# region-level validation AUROC, stop early on the region-level validation loss +# (patience 8 after a minimum of 10 epochs, set via trainer.min_epochs). +defaults: + - model_checkpoint + - early_stopping + - model_summary + - rich_progress_bar + - _self_ + +model_checkpoint: + dirpath: ${paths.output_dir}/checkpoints + filename: "epoch_{epoch:03d}" + monitor: "val/auroc" + mode: "max" + save_last: True + save_top_k: 1 + auto_insert_metric_name: False + +early_stopping: + monitor: "val/loss_region" + patience: 8 + mode: "min" + strict: False + +model_summary: + max_depth: -1 diff --git a/src/grass_mil/configs/callbacks/pretrain_checkpoint.yaml b/src/grass_mil/configs/callbacks/pretrain_checkpoint.yaml new file mode 100644 index 0000000..9c831eb --- /dev/null +++ b/src/grass_mil/configs/callbacks/pretrain_checkpoint.yaml @@ -0,0 +1,19 @@ +# Pretraining: a checkpoint every N optimizer steps (preprint: 5000), all kept, +# plus last.ckpt. No validation metric is involved. +defaults: + - model_checkpoint + - model_summary + - rich_progress_bar + - _self_ + +model_checkpoint: + dirpath: ${paths.output_dir}/checkpoints + filename: "step_{step:07d}" + monitor: null + save_last: True + save_top_k: -1 + every_n_train_steps: 5000 + auto_insert_metric_name: False + +model_summary: + max_depth: -1 diff --git a/src/grass_mil/configs/callbacks/refit.yaml b/src/grass_mil/configs/callbacks/refit.yaml new file mode 100644 index 0000000..9c0c44f --- /dev/null +++ b/src/grass_mil/configs/callbacks/refit.yaml @@ -0,0 +1,18 @@ +# Refit on all development folds for a fixed number of epochs: no early +# stopping, no metric-based selection; the final-epoch weights are the model. +defaults: + - model_checkpoint + - model_summary + - rich_progress_bar + - _self_ + +model_checkpoint: + dirpath: ${paths.output_dir}/checkpoints + filename: "last_epoch" + monitor: null + save_last: True + save_top_k: 0 + auto_insert_metric_name: False + +model_summary: + max_depth: -1 diff --git a/src/grass_mil/configs/data/sampler/ego_graph_mil.yaml b/src/grass_mil/configs/data/sampler/ego_graph_mil.yaml new file mode 100644 index 0000000..b02deae --- /dev/null +++ b/src/grass_mil/configs/data/sampler/ego_graph_mil.yaml @@ -0,0 +1,36 @@ +# Ego-graph bags for MIL fine-tuning (preprint defaults). +# Training: `subsample_count` random roots per region per epoch (uniform, not +# cell-type weighted), one batch per region (`unit_as_bag`), full k-hop +# ego-graphs (depth 4, unlimited neighbours, no radius cap). +# Validation: a fixed 1024-root subset per region (`val_sampler`, below). +# Test/predict: every root once (derived automatically from this config). +name: shadow_custom +kwargs: {} +runtime: + enabled: true + depth: 4 + num_neighbors: -1 # -1 = all neighbours at every hop + subgraph_batch_size: 64 # upper bound per chunk; ignored when unit_as_bag + replace: false + shuffle_subgraphs: true + proportional_root_sampling: false + property_name: cell_type + weight_mode: inverse + min_weight: 1.0e-6 + subsample_fraction: 1.0 + subsample_count: 256 + subsample_seed: null + fixed_per_unit_seed: null + unit_as_bag: true + merge_units: false + interior_seeds: + enabled: false + n_hops: null + seed_tolerance_multiplier: 1.0 + keep_largest_component: true + cache_dir: null + ego_radius: + enabled: false + radius_per_hop: 75.0 + radius_offset: 55.0 + keep_root_component: true diff --git a/src/grass_mil/configs/data/sampler/ego_graph_pretrain.yaml b/src/grass_mil/configs/data/sampler/ego_graph_pretrain.yaml new file mode 100644 index 0000000..1f79f87 --- /dev/null +++ b/src/grass_mil/configs/data/sampler/ego_graph_pretrain.yaml @@ -0,0 +1,33 @@ +# Ego-graph batches for BGRL pretraining (preprint "A3" defaults): roots drawn +# uniformly from the union of ALL regions (`merge_units`), 64 full k-hop +# ego-graphs per step (depth 4, unlimited neighbours, no radius cap). +name: shadow_custom +kwargs: {} +runtime: + enabled: true + depth: 4 + num_neighbors: -1 + subgraph_batch_size: 64 + replace: false + shuffle_subgraphs: true + proportional_root_sampling: false + property_name: cell_type + weight_mode: inverse + min_weight: 1.0e-6 + subsample_fraction: 1.0 + subsample_count: null + subsample_seed: null + fixed_per_unit_seed: null + unit_as_bag: false + merge_units: true + interior_seeds: + enabled: false + n_hops: null + seed_tolerance_multiplier: 1.0 + keep_largest_component: true + cache_dir: null + ego_radius: + enabled: false + radius_per_hop: 75.0 + radius_offset: 55.0 + keep_root_component: true diff --git a/src/grass_mil/configs/data/spatial_omics.yaml b/src/grass_mil/configs/data/spatial_omics.yaml index 1ad5f44..9cee887 100644 --- a/src/grass_mil/configs/data/spatial_omics.yaml +++ b/src/grass_mil/configs/data/spatial_omics.yaml @@ -1,3 +1,7 @@ +defaults: + - sampler: shadow_custom + - _self_ + _target_: grass_mil.data.spatial_omics_datamodule.SpatialOmicsDataModule data_dir: ${paths.data_dir} @@ -29,6 +33,15 @@ split: validation_strategy: heldout_fold_items # heldout_fold_items | patches_from_train_items val_ratio: 0.1 # exactly 0.0 disables val; non-zero guarantees at least one val item seed: 42 + # Explicit id lists (region ids by default, or sample ids with id_kind: sample) + # re-partition an existing precompute without re-processing. Each entry is a + # list or a path to a .json / .txt file; ids not listed are dropped. This is + # what `grass-mil-nested-cv` and `grass-mil-probe` use. + explicit: + train: null + val: null + test: null + id_kind: region # region | sample | patch (every tile its own unit) manifest: sample_id: sample_id @@ -74,36 +87,12 @@ feature_reducer: fit_mode: train_only # train_only | global kwargs: {} -sampler: - name: shadow_custom - kwargs: {} - runtime: - enabled: true - depth: 2 - num_neighbors: 8 - subgraph_batch_size: 32 - replace: false - shuffle_subgraphs: true - proportional_root_sampling: true - property_name: cell_type - weight_mode: inverse - min_weight: 1.0e-6 - # See configs/data/sampler/shadow_custom.yaml for the full documentation. - interior_seeds: - enabled: false - n_hops: null - seed_tolerance_multiplier: 1.0 - keep_largest_component: true - cache_dir: null - ego_radius: - enabled: false - radius_per_hop: 75.0 - radius_offset: 55.0 - keep_root_component: true - -# Optional validation-only sampler override. -# When null, validation reuses `sampler`. +# `sampler` comes from the data/sampler group (shadow_custom by default; +# `data/sampler=ego_graph_mil` is the preprint MIL setting). Override single +# keys as `data.sampler.runtime.=...`. val_sampler: null +# Test/predict loader; null derives an all-roots, unweighted variant of `sampler`. +test_sampler: null tiling: tile_size_um: 200.0 @@ -115,5 +104,6 @@ graph_labels: id_column: id tasks: null scope: region + patient_column: null # grouping column; attached as data.patient_id, never a target transforms: [] diff --git a/src/grass_mil/configs/eval.yaml b/src/grass_mil/configs/eval.yaml index b37d70d..5b34bd5 100644 --- a/src/grass_mil/configs/eval.yaml +++ b/src/grass_mil/configs/eval.yaml @@ -16,6 +16,10 @@ defaults: - paths: default - extras: default - hydra: default + - callbacks: none + # Regime presets compose here too (`regime=mil`), so the evaluated model + # matches the checkpoint. + - regime: null task_name: "eval" diff --git a/src/grass_mil/configs/experiment/debug.yaml b/src/grass_mil/configs/experiment/debug.yaml deleted file mode 100644 index 458a17c..0000000 --- a/src/grass_mil/configs/experiment/debug.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# @package _global_ - -defaults: - - override /data: spatial_omics - - override /model: null - - override /trainer: default - -data: - raw_manifest_path: ${paths.data_dir}/dummy/manifest.csv - processed_dir: ${paths.data_dir}/dummy/processed - coord_scale_um: 1.0 - sample_unit: tile - use_molecular_features: true - tiling: - tile_size_um: 300.0 - stride_um: 300.0 - categorical_features: - include_labels: ["cell type"] - h5ad: - coord_source: obsm - coord_key: spatial - categorical_label_columns: ["cell type"] - split: - train_val_test_split: [1.0, 0.0, 0.0] - split_by: sample diff --git a/src/grass_mil/configs/experiment/example_cohort.yaml b/src/grass_mil/configs/experiment/example_cohort.yaml new file mode 100644 index 0000000..324549b --- /dev/null +++ b/src/grass_mil/configs/experiment/example_cohort.yaml @@ -0,0 +1,53 @@ +# @package _global_ +# Template for a cohort of whole regions (cores, ROIs, slides) given as CSV cell +# tables. Copy this file, rename it (e.g. my_cohort.yaml), fill in the paths and +# column names, then select it on every command with `experiment=my_cohort`: +# +# grass-mil-train experiment=my_cohort regime=pretrain_a3 paths.output_dir=runs/pretrain_a3 +# grass-mil-nested-cv experiment=my_cohort regime=linear_ftall_reg model.init_from_ckpt=... +# grass-mil-extract experiment=my_cohort regime=linear_ftall_reg extract.nested_cv_summary=... +# +# Keys listed here are the ones that describe a dataset. Every other setting +# (encoder, ego-graphs, regimes, clustering) keeps its preset default and can +# still be overridden on the command line. Values are examples. + +paths: + data_dir: /path/to/my_cohort # holds raw/ and processed/ + +data: + # --- inputs ------------------------------------------------------------- + raw_manifest_path: ${paths.data_dir}/raw/manifest.csv # sample_id,input_path,input_type,region_id + processed_dir: ${paths.data_dir}/processed # graphs are written here once + sample_unit: full # full = one graph per region; tile = cut regions into tiles (see docs) + coord_scale_um: 1.0 # multiply coordinates by this to get the unit used below + min_cells: 10 # regions with fewer cells are dropped + csv: + sep: "," + coord_columns: [x, y] + cell_id_column: cell_id + categorical_label_columns: [cell_type] # the cell-type column (any name) + molecular_columns: [SIZE] # continuous node features; [] for cell type only + categorical_features: + include_labels: [cell_type] # must match the column above + # --- graph -------------------------------------------------------------- + graph_builder: + name: delaunay + kwargs: + edge_features: [log_distance, neighbor] # GRASS-MIL default + neighbor_cutoff_um: 55.0 # contact distance, in the coordinate unit after coord_scale_um + # --- labels and grouping ------------------------------------------------- + graph_labels: + label_file: ${paths.data_dir}/raw/labels.csv # REGION_ID,STAS,PATIENT + id_column: REGION_ID + tasks: [STAS] # target column(s); 0/1 or category names + scope: region + patient_column: PATIENT # grouping; never a target + split: + split_by: region # persisted random split used only when no explicit split is given + train_val_test_split: [0.8, 0.1, 0.1] + +nested_cv: + folds_file: ${paths.data_dir}/raw/folds.json # JSON list of lists of region ids, patient-grouped + +task_name: my_cohort +tags: [my_cohort] diff --git a/src/grass_mil/configs/experiment/grass_mil_pretrain_a3.yaml b/src/grass_mil/configs/experiment/grass_mil_pretrain_a3.yaml new file mode 100644 index 0000000..b158991 --- /dev/null +++ b/src/grass_mil/configs/experiment/grass_mil_pretrain_a3.yaml @@ -0,0 +1,21 @@ +# @package _global_ +# Same recipe as `regime=pretrain_a3`, for runs without a cohort experiment file: +# grass-mil-train experiment=grass_mil_pretrain_a3 data.raw_manifest_path=... data.processed_dir=... +# With a cohort file use `experiment=my_cohort regime=pretrain_a3` (one experiment +# file per command). Keep the two files identical. +defaults: + - override /model: bgrl_a3 + - override /task: pretrain_bgrl_a3 + - override /callbacks: pretrain_checkpoint + - override /data/sampler: ego_graph_pretrain + +regime_name: pretrain_a3 +task_name: pretrain_a3 +tags: ["grass_mil", "pretrain", "a3"] +test: false + +data: + split: + train_val_test_split: [1.0, 0.0, 0.0] + split_by: region + val_sampler: null diff --git a/src/grass_mil/configs/extract.yaml b/src/grass_mil/configs/extract.yaml new file mode 100644 index 0000000..65b515c --- /dev/null +++ b/src/grass_mil/configs/extract.yaml @@ -0,0 +1,33 @@ +# @package _global_ +# grass-mil-extract: per-cell cache (embeddings, instance logits, attention) of +# one or more trained models over ALL cells of every region. +# +# grass-mil-extract regime=linear_ftall_reg data=... \ +# extract.nested_cv_summary=/nested_cv_summary.json +# grass-mil-extract regime=mil data=... +extract.models.final=/path/to/last.ckpt +# +# The model is composed from the same `regime`/`model` keys as the training run +# so the checkpoint fits; the ego-graph settings come from `data.sampler`. +defaults: + - train + - _self_ + +task_name: "extract" +train: false +test: false + +extract: + # Either a nested-CV summary (one model per outer fold, held-out regions + # recorded) or an explicit {name: checkpoint} mapping. + nested_cv_summary: null + models: {} + # Optional held-out region lists per explicit model: +extract.held_out.=[...] + held_out: {} + output_dir: ${paths.output_dir}/cells + regions: null # optional list / json file of unit ids to extract (null = all) + # auto = patch when the precompute holds several tiles per region, else region. + unit_id: auto # auto | region | sample | patch + batch_size: 512 # ego-graphs per forward pass + autocast: true # fp16 autocast on GPU + device: auto + resume: true diff --git a/src/grass_mil/configs/inference/aggregation/default.yaml b/src/grass_mil/configs/inference/aggregation/default.yaml index 9f0eba7..e26132e 100644 --- a/src/grass_mil/configs/inference/aggregation/default.yaml +++ b/src/grass_mil/configs/inference/aggregation/default.yaml @@ -1,5 +1,15 @@ enabled: true -mode: mean # one of: mean, max, none, attention_weighted -bag_scope: region # one of: patch, region, sample +# mean | max | none | attention_weighted | mean_probability +mode: mean +# patch | region | sample | patient +# `patient` pools the regions of a patient: with mode=mean_probability the +# patient score is the mean of its regions' probabilities (preprint default); +# with mode=attention_weighted all cells of all regions form one merged bag. +bag_scope: region subsample_fraction: 1.0 # in (0, 1]; when <1.0, predict path applies pre-forward root subsampling for efficiency subsample_seed: null # optional seed for deterministic subsampling +# Region -> patient mapping used by bag_scope=patient (a label CSV). +patient_map: + label_file: null + id_column: REGION_ID + patient_column: PATIENT diff --git a/src/grass_mil/configs/inference/predict.yaml b/src/grass_mil/configs/inference/predict.yaml index 4bff41a..c20ba78 100644 --- a/src/grass_mil/configs/inference/predict.yaml +++ b/src/grass_mil/configs/inference/predict.yaml @@ -19,6 +19,10 @@ defaults: - aggregation: default - embeddings: default - interpretability: default + # Training-regime presets compose here too, so `regime=mil` rebuilds the + # exact model (head width, attention, sampler depth) the checkpoint was trained with. + - /callbacks: none + - /regime: null task_name: "predict" tags: ["dev"] diff --git a/src/grass_mil/configs/metaclusters.yaml b/src/grass_mil/configs/metaclusters.yaml new file mode 100644 index 0000000..37f7e83 --- /dev/null +++ b/src/grass_mil/configs/metaclusters.yaml @@ -0,0 +1,69 @@ +# grass-mil-metaclusters: cross-fold niche replication. +# +# grass-mil-metaclusters cells_dir=/cells labels.label_file=labels.csv \ +# labels.label_column=STAS labels.patient_column=PATIENT stage=all +# +# Stages: cluster (each fold model's held-out cells in its own space, fixed +# settings) -> match (Hellinger distance on composition, cannot-link within +# fold, threshold scanned or fixed) -> transfer (every model labels every +# region; train vs held-out, pairwise ARI, consensus patient shares) -> +# basis (shared label-free k-means basis with per-model attribution). +defaults: + - _self_ + - paths: default + - hydra: default + +task_name: metaclusters +cells_dir: ??? # directory holding one cache subdirectory per fold model +models: null # subset of model names; null = every cache with held-out regions +output_dir: ${paths.output_dir} +stage: all # all | cluster | match | transfer | basis +labels: + label_file: ??? # region labels (and patient column) + id_column: REGION_ID + label_column: ??? + patient_column: null +positive_class: 1 +positive_name: positive + +# Stage 1 clustering (same keys as grass-mil-niches) +reduction: + method: pca + n_components: 10 + random_state: 42 +clustering: + method: hdbscan + n_clusters: null + random_state: 42 + params: + min_cluster_size: 300 + min_samples: 1 + cluster_selection_method: eom +sample_size: 100000 +l2_normalise: true +assign_k: 15 +seed: 0 +n_boot: 2000 + +# Stage 2 matching +match: + distance: hellinger # hellinger | aitchison + threshold: null # fixed threshold (e.g. 0.40); null = scan + scan: {min: 0.15, max: 0.6, n: 46} + min_folds: 3 # "replicated" = members from at least this many folds + +# Stage 3 transfer +transfer: + min_cells: 100 + present_threshold: 0.02 + +# Stage 4 shared basis +basis: + enabled: true + space_model: null # model name whose embedding defines the basis (null = first); or a cache dir + k: 20 + sample_size: 300000 + n_init: 10 + seed: 0 + +figures: true diff --git a/src/grass_mil/configs/model/attention/gated.yaml b/src/grass_mil/configs/model/attention/gated.yaml index db41112..695305f 100644 --- a/src/grass_mil/configs/model/attention/gated.yaml +++ b/src/grass_mil/configs/model/attention/gated.yaml @@ -1,7 +1,7 @@ -_target_: grass_mil.models.components.attention.AttnNetGated -input_dim: 128 +# Plain gated attention (Ilse et al. 2018), one attention channel per class. +attention_type: gated +input_dim: ${model.encoder.out_dim} hidden_dim: 64 dropout: false -# Attention is per-class: n_classes must equal the graph head's output_dim. -# The task preset sets both together (finetune_mil uses 2 for binary). n_classes: 1 +init: kaiming diff --git a/src/grass_mil/configs/model/attention/gated_projected.yaml b/src/grass_mil/configs/model/attention/gated_projected.yaml index 85478c6..ce1365d 100644 --- a/src/grass_mil/configs/model/attention/gated_projected.yaml +++ b/src/grass_mil/configs/model/attention/gated_projected.yaml @@ -1,8 +1,10 @@ -_target_: grass_mil.models.components.attention.AttnNetGatedProjected -input_dim: 128 -projection_dim: 64 -hidden_dim: 16 -dropout: false -# Attention is per-class: n_classes must equal the graph head's output_dim. -# The task preset sets both together (finetune_mil uses 2 for binary). +# Projected gated attention (GRASS-MIL default): Linear + LeakyReLU projection, +# tanh/sigmoid gate, one attention channel per class. `n_classes` must equal +# graph_head.output_dim; the task preset sets both together. +attention_type: gated_projected +input_dim: ${model.encoder.out_dim} +projection_dim: 32 +hidden_dim: 8 +dropout: true n_classes: 1 +init: kaiming diff --git a/src/grass_mil/configs/model/bgrl_a3.yaml b/src/grass_mil/configs/model/bgrl_a3.yaml new file mode 100644 index 0000000..30b179d --- /dev/null +++ b/src/grass_mil/configs/model/bgrl_a3.yaml @@ -0,0 +1,28 @@ +defaults: + - encoder: gine_length + - _self_ + +# BGRL pretraining of the GRASS-MIL encoder (preprint "A3" recipe): the same +# GINE ego-graph encoder as model/grass_mil, encoder dropout 0.1 during +# pretraining only, predictor Linear(256->512) -> PReLU -> Linear(512->256). +# Use with `task=pretrain_bgrl_a3` (experiment=grass_mil_pretrain_a3 bundles +# model, task, sampler and callbacks). Every key can be overridden. + +_target_: grass_mil.models.bgrl_module.BGRLModule +_recursive_: false + +encoder: + dropout: 0.1 + +ssl: + method: bgrl + predictor: + hidden_size: 512 + +optim: ${oc.select:optim,null} +scheduler: ${oc.select:scheduler,null} +task: ${task} + +init_from_ckpt: null +init_strict: false +encoder_init_map: auto_bgrl_or_identity diff --git a/src/grass_mil/configs/model/bgrl_module.yaml b/src/grass_mil/configs/model/bgrl_module.yaml index 1f4c1a8..300c4d5 100644 --- a/src/grass_mil/configs/model/bgrl_module.yaml +++ b/src/grass_mil/configs/model/bgrl_module.yaml @@ -1,27 +1,12 @@ +defaults: + - encoder: gin + - _self_ + _target_: grass_mil.models.bgrl_module.BGRLModule _recursive_: false -encoder: - input_dim: 0 - hidden_dim: 128 - out_dim: 128 - num_layers: 3 - dropout: 0.1 - conv_type: gin - norm: batchnorm - jk: last - act: relu - pooling: mean - gat_heads: 4 - use_edge_attr: false - edge_weight_index: 0 - edge_attr_dim: null - # Selects a single scalar edge column (the edge length) for GINE message - # passing; null passes the whole edge_attr vector through. - edge_feature_index: null - # Learned embedding of a categorical node label, summed with the projection - # of the continuous features. null disables it (features come from `x` only). - categorical_embedding: null +# `encoder` comes from the model/encoder group (override with +# `model/encoder=gine_length`). ssl: method: bgrl diff --git a/src/grass_mil/configs/model/components.yaml b/src/grass_mil/configs/model/components.yaml deleted file mode 100644 index 2116e22..0000000 --- a/src/grass_mil/configs/model/components.yaml +++ /dev/null @@ -1,54 +0,0 @@ -# @package _global_ - -model: - flags: - use_attention: true - use_ssl: false - - encoder: - _target_: grass_mil.models.components.backbones.EncoderConfig - input_dim: 64 - hidden_dim: 128 - out_dim: 128 - num_layers: 3 - dropout: 0.1 - conv_type: gin - norm: batchnorm - jk: last - act: relu - pooling: mean - gat_heads: 4 - use_edge_attr: false - edge_weight_index: 0 - edge_attr_dim: null - # Selects a single scalar edge column (the edge length) for GINE message - # passing; null passes the whole edge_attr vector through. - edge_feature_index: null - # Learned embedding of a categorical node label, summed with the projection - # of the continuous features. null disables it (features come from `x` only). - categorical_embedding: null - - attention: - _target_: grass_mil.models.components.attention.AttnNetGatedProjected - input_dim: 128 - projection_dim: 64 - hidden_dim: 16 - dropout: false - # Must equal graph_head.output_dim: attention is per-class. - n_classes: 1 - - ssl: - method: bgrl - predictor: - _target_: grass_mil.models.components.ssl.MLPPredictor - input_size: 128 - output_size: 128 - hidden_size: 512 - - graph_head: - _target_: grass_mil.models.components.heads.GraphPredictionHead - input_dim: 128 - output_dim: 1 - hidden_dim: 128 - num_layers: 2 - dropout: 0.0 diff --git a/src/grass_mil/configs/model/encoder/gat.yaml b/src/grass_mil/configs/model/encoder/gat.yaml index dd080d4..ca9dbda 100644 --- a/src/grass_mil/configs/model/encoder/gat.yaml +++ b/src/grass_mil/configs/model/encoder/gat.yaml @@ -1,5 +1,4 @@ -_target_: grass_mil.models.components.backbones.EncoderConfig -input_dim: 64 +input_dim: 0 hidden_dim: 128 out_dim: 128 num_layers: 3 @@ -13,3 +12,7 @@ gat_heads: 4 use_edge_attr: false edge_weight_index: 0 edge_attr_dim: null +edge_feature_index: null +categorical_embedding: null +train_eps: true +input_init: xavier diff --git a/src/grass_mil/configs/model/encoder/gcn.yaml b/src/grass_mil/configs/model/encoder/gcn.yaml index 75d9b27..7ceb192 100644 --- a/src/grass_mil/configs/model/encoder/gcn.yaml +++ b/src/grass_mil/configs/model/encoder/gcn.yaml @@ -1,5 +1,4 @@ -_target_: grass_mil.models.components.backbones.EncoderConfig -input_dim: 64 +input_dim: 0 hidden_dim: 128 out_dim: 128 num_layers: 3 @@ -13,3 +12,7 @@ gat_heads: 4 use_edge_attr: true edge_weight_index: 0 edge_attr_dim: null +edge_feature_index: null +categorical_embedding: null +train_eps: true +input_init: xavier diff --git a/src/grass_mil/configs/model/encoder/gin.yaml b/src/grass_mil/configs/model/encoder/gin.yaml index ec63d74..946517e 100644 --- a/src/grass_mil/configs/model/encoder/gin.yaml +++ b/src/grass_mil/configs/model/encoder/gin.yaml @@ -1,5 +1,6 @@ -_target_: grass_mil.models.components.backbones.EncoderConfig -input_dim: 64 +# Small GIN encoder without edge features: a lightweight default for smoke runs. +# `input_dim: 0` lets the datamodule report the continuous node-feature width. +input_dim: 0 hidden_dim: 128 out_dim: 128 num_layers: 3 @@ -13,3 +14,7 @@ gat_heads: 4 use_edge_attr: false edge_weight_index: 0 edge_attr_dim: null +edge_feature_index: null +categorical_embedding: null +train_eps: true +input_init: xavier diff --git a/src/grass_mil/configs/model/encoder/gine.yaml b/src/grass_mil/configs/model/encoder/gine.yaml index 6d5546e..e4996c7 100644 --- a/src/grass_mil/configs/model/encoder/gine.yaml +++ b/src/grass_mil/configs/model/encoder/gine.yaml @@ -1,5 +1,4 @@ -_target_: grass_mil.models.components.backbones.EncoderConfig -input_dim: 64 +input_dim: 0 hidden_dim: 128 out_dim: 128 num_layers: 3 @@ -13,3 +12,7 @@ gat_heads: 4 use_edge_attr: true edge_weight_index: 0 edge_attr_dim: 2 +edge_feature_index: null +categorical_embedding: null +train_eps: true +input_init: xavier diff --git a/src/grass_mil/configs/model/encoder/gine_length.yaml b/src/grass_mil/configs/model/encoder/gine_length.yaml index 02ec00b..5b468c7 100644 --- a/src/grass_mil/configs/model/encoder/gine_length.yaml +++ b/src/grass_mil/configs/model/encoder/gine_length.yaml @@ -1,17 +1,18 @@ -# Production GRASS-MIL encoder: GINE conditioned on the scalar edge length, -# with cell identity supplied through a learned embedding rather than as a -# one-hot column of `x`. +# GRASS-MIL encoder (preprint defaults): 4 GINE layers of width 256 conditioned +# on the bounded log edge length, layernorm, max pooling over the ego-graph, +# cell identity through a learned embedding summed with the projection of the +# continuous node features. Every value is a plain Hydra key and can be +# overridden (e.g. `model.encoder.num_layers=3 model.encoder.hidden_dim=128`). # # `input_dim: 0` lets the datamodule report the continuous node-feature width # (0 for cell-type-only cohorts, 1 when cell size is present). Likewise # `num_embeddings: null` and `column_index: null` are resolved from the # processed dataset's label map and categorical slices. -_target_: grass_mil.models.components.backbones.EncoderConfig input_dim: 0 hidden_dim: 256 out_dim: 256 num_layers: 4 -dropout: 0.2 +dropout: 0.0 conv_type: gine norm: layernorm jk: last @@ -19,18 +20,26 @@ act: relu pooling: max gat_heads: 4 -# Message passing reads exactly one edge column: the edge length. Index 0 of -# `edge_attr` is `distance` for the delaunay graph builder's default -# `edge_features: ["distance", "neighbor"]`. +# Message passing reads exactly one edge column. With the graph builder's +# `edge_features: [log_distance, neighbor]` (preprint), index 0 is the bounded +# log edge length clip((ln d - 2)/3, 0, 1). use_edge_attr: true edge_weight_index: 0 edge_attr_dim: 1 edge_feature_index: 0 +# Learnable GIN epsilon and xavier-initialised input embeddings, as in the preprint. +train_eps: true +input_init: xavier + categorical_embedding: label: cell_type num_embeddings: null column_index: null - # Reserve a trailing row so BGRL augmentations can mark a dropped node - # "unassigned" without resizing the table. + # Reserve a trailing row so augmentations can mark a dropped node + # "unassigned" without resizing the table. Alternatively set + # `reserve_unassigned: false` and `unassigned_label: other` to reuse an + # existing category as the unassigned token. reserve_unassigned: true + unassigned_label: null + unassigned_index: null diff --git a/src/grass_mil/configs/model/encoder/graphsage.yaml b/src/grass_mil/configs/model/encoder/graphsage.yaml index 88df378..12fcdc9 100644 --- a/src/grass_mil/configs/model/encoder/graphsage.yaml +++ b/src/grass_mil/configs/model/encoder/graphsage.yaml @@ -1,5 +1,4 @@ -_target_: grass_mil.models.components.backbones.EncoderConfig -input_dim: 64 +input_dim: 0 hidden_dim: 128 out_dim: 128 num_layers: 3 @@ -13,3 +12,7 @@ gat_heads: 4 use_edge_attr: false edge_weight_index: 0 edge_attr_dim: null +edge_feature_index: null +categorical_embedding: null +train_eps: true +input_init: xavier diff --git a/src/grass_mil/configs/model/grass_mil.yaml b/src/grass_mil/configs/model/grass_mil.yaml new file mode 100644 index 0000000..1c61989 --- /dev/null +++ b/src/grass_mil/configs/model/grass_mil.yaml @@ -0,0 +1,45 @@ +defaults: + - encoder: gine_length + - attention: gated_projected + - _self_ + +# GRASS-MIL supervised module with the preprint defaults: BGRL-pretrainable GINE +# ego-graph encoder (model/encoder/gine_length), projected gated attention with +# one channel per class (model/attention/gated_projected), and an MLP instance +# head pooled in logit space (bag logit_c = sum_i A[i,c] * f_c(h_i)), so each +# prediction decomposes exactly into per-neighbourhood contributions. +# +# Use with `model=grass_mil task=finetune_mil` (and a `regime=...` preset). Any +# key can be overridden, e.g. `model.encoder.num_layers=3`, +# `model.graph_head.num_layers=1` (linear head for the probe regimes). + +_target_: grass_mil.models.supervised_module.SupervisedModule +_recursive_: false + +flags: + use_ssl: false + +graph_head: + input_dim: ${model.encoder.out_dim} + output_dim: 1 + hidden_dim: ${model.encoder.out_dim} + # 2 = MLP instance head (default); 1 = linear head for grass-mil-probe regimes. + num_layers: 2 + dropout: 0.0 + +ssl: + method: bgrl + predictor: + hidden_size: 512 + +loss: + loss_type: ${task.loss} + +optim: ${oc.select:optim,null} +scheduler: ${oc.select:scheduler,null} +task: ${task} + +init_from_ckpt: null +init_strict: false +encoder_init_map: auto_bgrl_or_identity +freeze_encoder: false diff --git a/src/grass_mil/configs/model/heads/graph.yaml b/src/grass_mil/configs/model/heads/graph.yaml deleted file mode 100644 index 26752d9..0000000 --- a/src/grass_mil/configs/model/heads/graph.yaml +++ /dev/null @@ -1,6 +0,0 @@ -_target_: grass_mil.models.components.heads.GraphPredictionHead -input_dim: 128 -output_dim: 1 -hidden_dim: 128 -num_layers: 2 -dropout: 0.0 diff --git a/src/grass_mil/configs/model/loss/categorical_bce.yaml b/src/grass_mil/configs/model/loss/categorical_bce.yaml deleted file mode 100644 index 3ebaef1..0000000 --- a/src/grass_mil/configs/model/loss/categorical_bce.yaml +++ /dev/null @@ -1,2 +0,0 @@ -_target_: grass_mil.models.components.losses.WeightedBCEWithLogitsLoss -pos_weight: null diff --git a/src/grass_mil/configs/model/loss/categorical_ce.yaml b/src/grass_mil/configs/model/loss/categorical_ce.yaml deleted file mode 100644 index 28218f2..0000000 --- a/src/grass_mil/configs/model/loss/categorical_ce.yaml +++ /dev/null @@ -1,2 +0,0 @@ -_target_: grass_mil.models.components.losses.WeightedCrossEntropyLoss -class_weight: null diff --git a/src/grass_mil/configs/model/loss/regression_huber.yaml b/src/grass_mil/configs/model/loss/regression_huber.yaml deleted file mode 100644 index da58ab7..0000000 --- a/src/grass_mil/configs/model/loss/regression_huber.yaml +++ /dev/null @@ -1,2 +0,0 @@ -_target_: grass_mil.models.components.losses.WeightedHuberLoss -delta: 1.0 diff --git a/src/grass_mil/configs/model/loss/regression_mse.yaml b/src/grass_mil/configs/model/loss/regression_mse.yaml deleted file mode 100644 index c459c7e..0000000 --- a/src/grass_mil/configs/model/loss/regression_mse.yaml +++ /dev/null @@ -1 +0,0 @@ -_target_: grass_mil.models.components.losses.WeightedMSELoss diff --git a/src/grass_mil/configs/model/loss/survival_coxsgd.yaml b/src/grass_mil/configs/model/loss/survival_coxsgd.yaml deleted file mode 100644 index 0ac72a6..0000000 --- a/src/grass_mil/configs/model/loss/survival_coxsgd.yaml +++ /dev/null @@ -1,3 +0,0 @@ -_target_: grass_mil.models.components.losses.CoxSGDLoss -top_n: 5 -regularizer_weight: 0.05 diff --git a/src/grass_mil/configs/model/ssl/bgrl.yaml b/src/grass_mil/configs/model/ssl/bgrl.yaml deleted file mode 100644 index fc77634..0000000 --- a/src/grass_mil/configs/model/ssl/bgrl.yaml +++ /dev/null @@ -1,6 +0,0 @@ -method: bgrl -predictor: - _target_: grass_mil.models.components.ssl.MLPPredictor - input_size: 128 - output_size: 128 - hidden_size: 512 diff --git a/src/grass_mil/configs/model/supervised_module.yaml b/src/grass_mil/configs/model/supervised_module.yaml index e74132f..1f6d0e2 100644 --- a/src/grass_mil/configs/model/supervised_module.yaml +++ b/src/grass_mil/configs/model/supervised_module.yaml @@ -1,45 +1,24 @@ +defaults: + - encoder: gin + - attention: gated_projected + - _self_ + _target_: grass_mil.models.supervised_module.SupervisedModule _recursive_: false flags: use_ssl: false -encoder: - input_dim: 0 - hidden_dim: 128 - out_dim: 128 - num_layers: 3 - dropout: 0.1 - conv_type: gin - norm: batchnorm - jk: last - act: relu - pooling: mean - gat_heads: 4 - use_edge_attr: false - edge_weight_index: 0 - edge_attr_dim: null - # Selects a single scalar edge column (the edge length) for GINE message - # passing; null passes the whole edge_attr vector through. - edge_feature_index: null - # Learned embedding of a categorical node label, summed with the projection - # of the continuous features. null disables it (features come from `x` only). - categorical_embedding: null - -# Attention block -- only used when task.aggregation == mil_attention. -attention: - attention_type: gated_projected - input_dim: 128 - projection_dim: 64 - hidden_dim: 16 - dropout: false - # Must equal graph_head.output_dim: attention is per-class. - n_classes: 1 +# `encoder` and `attention` come from the model/encoder and model/attention +# groups (override with `model/encoder=gine_length`, `model/attention=gated`). +# The attention block is only used when task.aggregation == mil_attention. graph_head: - input_dim: 128 + input_dim: ${model.encoder.out_dim} output_dim: 1 - hidden_dim: 128 + hidden_dim: ${model.encoder.out_dim} + # 2 = MLP instance head (default); 1 = linear head, required by the probe + # pre-fit regimes (grass-mil-probe folds a logistic regression into it). num_layers: 2 dropout: 0.0 diff --git a/src/grass_mil/configs/nested_cv.yaml b/src/grass_mil/configs/nested_cv.yaml new file mode 100644 index 0000000..9643f40 --- /dev/null +++ b/src/grass_mil/configs/nested_cv.yaml @@ -0,0 +1,55 @@ +# @package _global_ +# grass-mil-nested-cv: the preprint's nested, patient-grouped cross-validation. +# +# grass-mil-nested-cv regime=linear_ftall_reg data=... \ +# model.init_from_ckpt=/path/to/bgrl.ckpt \ +# nested_cv.folds_file=folds.json \ +# data.graph_labels.label_file=labels.csv data.graph_labels.id_column=REGION_ID \ +# data.graph_labels.tasks=[STAS] data.graph_labels.patient_column=PATIENT +# +# For every outer fold: the remaining folds form the development set, which is +# split into `inner_k` stratified, patient-grouped inner folds; each inner run +# trains with the selected regime and records the epoch with the best +# `selection_metric`; the median of those epochs is the refit length; the model +# is refitted on the whole development set for that many epochs (no early +# stopping, last weights) and scored once on the held-out fold, on all cells. +# The linear_* regimes fit their probe head on each training split first +# (cached under `probe.cache_dir`, so a second regime reuses them). +defaults: + - train + - _self_ + +task_name: "nested_cv" + +nested_cv: + folds_file: ??? # JSON list of lists of unit ids (patient-grouped outer folds) + # What one unit (bag, label, fold member) is: a region (one graph per region, + # data.sample_unit=full), a sample, or a patch (tiles as units; sets + # task.bag_key=patch_id and data.graph_labels.scope=patch). + id_kind: region + label_file: ${data.graph_labels.label_file} + id_column: ${data.graph_labels.id_column} + label_column: null # null = task.target_columns[0] or data.graph_labels.tasks[0] + patient_column: ${data.graph_labels.patient_column} + outer_folds: null # e.g. [2] to run one outer fold; null = all + inner_k: 3 + inner_seed: 0 + selection_metric: val/auroc + selection_mode: max # max | min + epoch_reduce: median # median | mean | max (over the inner folds' best epochs) + min_refit_epochs: 1 + resume: true # skip inner runs / outer folds whose results already exist + probe: + # auto = fit a probe wherever the regime asks for one (task.head_init_path + # left unset, as in the linear_* regimes); true = always; false = never. + enabled: auto + cache_dir: ${paths.output_dir}/probes + roots_per_unit: 96 + seed: 0 + batch_size: 256 + C: 1.0 + class_weight: balanced + max_iter: 3000 + device: auto + patient_aggregation: mean # mean of the region probabilities per patient + summary_filename: nested_cv_summary.json diff --git a/src/grass_mil/configs/niches.yaml b/src/grass_mil/configs/niches.yaml new file mode 100644 index 0000000..57a9c57 --- /dev/null +++ b/src/grass_mil/configs/niches.yaml @@ -0,0 +1,54 @@ +# grass-mil-niches: niche discovery on a trained model's cell cache. +# +# grass-mil-niches cells_dir=/cells/fold2 output_dir=niches_fold2 +# grass-mil-niches cells_dir=... clustering.method=kmeans clustering.n_clusters=12 +# grass-mil-niches cells_dir=... space=attention attention_ckpt=/checkpoints/last.ckpt +# +# The defaults are the preprint's recipe; every reduction / clustering key is +# a plain override (any backend of grass_mil.interpretability.core.clustering). +defaults: + - _self_ + - paths: default + - hydra: default + +task_name: niches +cells_dir: ??? # one model's cache directory written by grass-mil-extract +output_dir: ${paths.output_dir} +regions: held_out # held_out | all | list of region ids | path to .json/.txt +labels: + # Optional label CSV overriding the labels stored in the cache. + label_file: null + id_column: REGION_ID + label_column: null +space: gnn # gnn (encoder embedding) | attention (8-d pre-tanh attention space) +attention_ckpt: null # checkpoint of the model; required for space=attention +positive_class: 1 +positive_name: positive +title: null # report title (default from the cache name) +held_out_auroc: null # optional, printed in the report header + +reduction: + method: pca # pca | none | umap + n_components: 10 + random_state: 42 +clustering: + method: hdbscan # hdbscan | kmeans | agglomerative | dbscan | optics | spectral + n_clusters: null # for kmeans / agglomerative / spectral + random_state: 42 + params: + min_cluster_size: 300 + min_samples: 1 + cluster_selection_method: eom +sample_size: 100000 # cells used to fit the reducer and the clustering (null = all) +l2_normalise: true # cluster on L2-normalised coordinates (cosine geometry) +assign_k: 15 # k-NN assignment of unsampled cells on the reduced coordinates +seed: 0 + +present_threshold: 0.02 # a niche is "present" in a region above this share +n_boot: 2000 # bootstrap over regions for the margin CI +figures: true +umap: + enabled: true + n_neighbors: 30 + min_dist: 0.1 + metric: cosine diff --git a/src/grass_mil/configs/probe.yaml b/src/grass_mil/configs/probe.yaml new file mode 100644 index 0000000..cdaaffa --- /dev/null +++ b/src/grass_mil/configs/probe.yaml @@ -0,0 +1,29 @@ +# @package _global_ +# grass-mil-probe: fit the linear probe head on a frozen encoder. +# +# grass-mil-probe regime=linear_probehead data=... \ +# model.init_from_ckpt=/path/to/bgrl.ckpt \ +# data.split.explicit.train=train_ids.json data.split.explicit.val=val_ids.json +# +# The encoder is taken from `model` (with `init_from_ckpt`) and never trained; +# the ego-graph settings come from `data.sampler.runtime` (depth, neighbours, +# radius cap). The result is written as `graph_head.net.0.{weight,bias}` and is +# consumed through `task.head_init_path` by the linear_* regimes. +defaults: + - train + - _self_ + +task_name: "probe" +train: false +test: false + +probe: + roots_per_unit: 96 # ego-graphs per region used to fit the probe (null = all roots) + seed: 0 # fixed per-region root subset + batch_size: 256 # ego-graphs per forward pass + C: 1.0 # inverse L2 strength of the logistic regression + class_weight: balanced # balanced | null + max_iter: 3000 + eval_splits: [val] # splits scored after the fit (instance and region-mean AUROC) + output_path: ${paths.output_dir}/probe_head.pt + device: auto # auto | cpu | cuda diff --git a/src/grass_mil/configs/regime/linear_ftall.yaml b/src/grass_mil/configs/regime/linear_ftall.yaml new file mode 100644 index 0000000..bbea138 --- /dev/null +++ b/src/grass_mil/configs/regime/linear_ftall.yaml @@ -0,0 +1,28 @@ +# @package _global_ +# Preprint "ftALL": encoder fine-tuned end to end (lr x1.0), linear head +# initialised from the probe pre-fit and trained slowly (lr x0.01, wd 1e-3), +# attention at lr x2.0. +defaults: + - mil + +regime_name: linear_ftall + +task: + # "auto" = fitted per split by grass-mil-nested-cv; for a single grass-mil-train + # run pass the output of grass-mil-probe (task.head_init_path=/path/probe_head.pt). + head_init_path: auto + freeze_head: false + optimization: + encoder_lr_scale: 1.0 + head_lr_scale: 0.01 + attention_lr_scale: 2.0 + head_weight_decay: 1.0e-3 + +model: + graph_head: + num_layers: 1 + +data: + sampler: + runtime: + subsample_count: 256 diff --git a/src/grass_mil/configs/regime/linear_ftall_reg.yaml b/src/grass_mil/configs/regime/linear_ftall_reg.yaml new file mode 100644 index 0000000..9233540 --- /dev/null +++ b/src/grass_mil/configs/regime/linear_ftall_reg.yaml @@ -0,0 +1,21 @@ +# @package _global_ +# Preprint "ftALL, lr x0.3 + regularisation" (the STAS model of record): +# encoder lr x0.3, weight decay 1e-2, encoder dropout 0.2, edge/feature drop 0.05. +defaults: + - linear_ftall + +regime_name: linear_ftall_reg + +optim: + weight_decay: 1.0e-2 + +task: + optimization: + encoder_lr_scale: 0.3 + finetune_augmentation: + drop_edge_p: 0.05 + drop_feat_p: 0.05 + +model: + encoder: + dropout: 0.2 diff --git a/src/grass_mil/configs/regime/linear_probehead.yaml b/src/grass_mil/configs/regime/linear_probehead.yaml new file mode 100644 index 0000000..63887c8 --- /dev/null +++ b/src/grass_mil/configs/regime/linear_probehead.yaml @@ -0,0 +1,27 @@ +# @package _global_ +# Preprint "probehead": frozen encoder, LINEAR head pre-fitted by grass-mil-probe +# (task.head_init_path) and frozen, so the attention is the only trainable part. +# Its uniform-attention starting point is exactly the mean-pooled probe. +defaults: + - mil + +regime_name: linear_probehead + +task: + # "auto" = fitted per split by grass-mil-nested-cv; for a single grass-mil-train + # run pass the output of grass-mil-probe (task.head_init_path=/path/probe_head.pt). + head_init_path: auto + freeze_head: true + optimization: + encoder_lr_scale: 0.0 + head_lr_scale: 0.0 + attention_lr_scale: 2.0 + +model: + graph_head: + num_layers: 1 + +data: + sampler: + runtime: + subsample_count: 512 diff --git a/src/grass_mil/configs/regime/mil.yaml b/src/grass_mil/configs/regime/mil.yaml new file mode 100644 index 0000000..fc2182d --- /dev/null +++ b/src/grass_mil/configs/regime/mil.yaml @@ -0,0 +1,65 @@ +# @package _global_ +# GRASS-MIL default regime: BGRL-initialised encoder fine-tuned end to end, +# MLP instance head, projected gated attention, one region per bag. +# Every value below is a plain override and can be changed on the command line +# (e.g. `regime=mil task.region_accumulation.hyperbatch_size=8`). +defaults: + - override /model: grass_mil + - override /task: finetune_mil + - override /scheduler: cosine_mil_step + - override /callbacks: mil_selection + - override /data/sampler: ego_graph_mil + +regime_name: mil + +optim: + lr: 1.0e-4 + weight_decay: 1.0e-3 + +task: + optimization: + encoder_lr_scale: 1.0 + head_lr_scale: 1.0 + attention_lr_scale: 2.0 + head_weight_decay: null + finetune_augmentation: + drop_edge_p: 0.0 + drop_feat_p: 0.0 + region_accumulation: + hyperbatch_size: 12 + grad_clip_norm: 1.0 + +model: + encoder: + dropout: 0.0 + graph_head: + num_layers: 2 + +data: + sampler: + runtime: + subsample_count: 256 + # Validation on a FIXED 1024-root subset per region, so epoch-to-epoch metric + # differences reflect the model rather than the root sample. + val_sampler: + name: shadow_custom + kwargs: {} + runtime: + enabled: true + depth: ${data.sampler.runtime.depth} + num_neighbors: ${data.sampler.runtime.num_neighbors} + subgraph_batch_size: 64 + replace: false + shuffle_subgraphs: false + proportional_root_sampling: false + subsample_count: 1024 + fixed_per_unit_seed: 0 + unit_as_bag: false + merge_units: false + interior_seeds: ${data.sampler.runtime.interior_seeds} + ego_radius: ${data.sampler.runtime.ego_radius} + +trainer: + max_epochs: 25 + min_epochs: 10 + precision: 16-mixed diff --git a/src/grass_mil/configs/regime/mil_frozen.yaml b/src/grass_mil/configs/regime/mil_frozen.yaml new file mode 100644 index 0000000..afde9f8 --- /dev/null +++ b/src/grass_mil/configs/regime/mil_frozen.yaml @@ -0,0 +1,17 @@ +# @package _global_ +# Frozen encoder (learning-rate scale 0, kept in eval mode); MLP head and +# attention trained on the pretrained embeddings. Larger bags fit in memory +# because no activations are stored for the encoder. +defaults: + - mil + +regime_name: mil_frozen + +task: + optimization: + encoder_lr_scale: 0.0 + +data: + sampler: + runtime: + subsample_count: 512 diff --git a/src/grass_mil/configs/regime/pretrain_a3.yaml b/src/grass_mil/configs/regime/pretrain_a3.yaml new file mode 100644 index 0000000..a7b78ab --- /dev/null +++ b/src/grass_mil/configs/regime/pretrain_a3.yaml @@ -0,0 +1,22 @@ +# @package _global_ +# GRASS-MIL encoder pretraining (preprint "A3"): BGRL on the ego-graphs of +# every region, label-free. Composes with a cohort file: +# grass-mil-train experiment=my_cohort regime=pretrain_a3 paths.output_dir=runs/pretrain_a3 trainer=gpu +# Every value is overridable, e.g. task.total_steps=50000 model.encoder.num_layers=3 +# data.sampler.runtime.depth=3 task.augmentation.feature_noise_columns=[]. +defaults: + - override /model: bgrl_a3 + - override /task: pretrain_bgrl_a3 + - override /callbacks: pretrain_checkpoint + - override /data/sampler: ego_graph_pretrain + +regime_name: pretrain_a3 +task_name: pretrain_a3 +tags: ["grass_mil", "pretrain", "a3"] +test: false + +data: + split: + train_val_test_split: [1.0, 0.0, 0.0] + split_by: region + val_sampler: null diff --git a/src/grass_mil/configs/scheduler/cosine_mil_step.yaml b/src/grass_mil/configs/scheduler/cosine_mil_step.yaml new file mode 100644 index 0000000..cc5789d --- /dev/null +++ b/src/grass_mil/configs/scheduler/cosine_mil_step.yaml @@ -0,0 +1,6 @@ +# Cosine annealing per optimizer step over the whole run (preprint): with +# task.scheduler_interval=step, `T_max: auto` resolves to +# max_epochs * ceil(n_train_regions / hyperbatch_size). +_target_: torch.optim.lr_scheduler.CosineAnnealingLR +T_max: auto +eta_min: 1.0e-7 diff --git a/src/grass_mil/configs/scores.yaml b/src/grass_mil/configs/scores.yaml new file mode 100644 index 0000000..ee18ab7 --- /dev/null +++ b/src/grass_mil/configs/scores.yaml @@ -0,0 +1,29 @@ +# grass-mil-scores: evaluation statistics for held-out scores. +# +# grass-mil-scores input=/oof_predictions.csv +# grass-mil-scores input=table.csv scores=[grass,clinical] strata=[subtype] +# +# Input: a CSV with one row per unit (region or patient) holding a label +# column, one or more score columns, optionally a fold column (for within-fold +# standardisation and per-fold AUROCs), a patient column (scores are averaged +# per patient first when `patient_level: true`) and strata columns. +defaults: + - _self_ + - paths: default + - hydra: default + +task_name: scores +input: ??? +output_dir: ${paths.output_dir} +label_column: label +scores: [prob_class_1] # score columns to evaluate +fold_column: outer # null = no folds +patient_column: patient_id # null = evaluate rows as they are +patient_level: true # average region scores per patient before everything else +standardise: z # z | rank | none (within fold, before pooling) +strata: [] # optional columns; AUROC reported within each level +n_boot: 2000 +seed: 0 +operating_rules: [0.2, 0.3, 0.4, 0.5, 0.6] # flag the top q within fold +prevalences: [0.1, 0.2, 0.3, 0.4, 0.5] # PPV/NPV at other prevalences (first score) +report_filename: report_scores.md diff --git a/src/grass_mil/configs/spatial_stats.yaml b/src/grass_mil/configs/spatial_stats.yaml new file mode 100644 index 0000000..7b55016 --- /dev/null +++ b/src/grass_mil/configs/spatial_stats.yaml @@ -0,0 +1,40 @@ +# grass-mil-spatial-stats: classical spatial statistics per region, compared +# between classes at the patient level. +# +# grass-mil-spatial-stats cells_dir=/cells/ \ +# labels.label_file=labels.csv labels.label_column=STAS labels.patient_column=PATIENT \ +# reference=[tumor] partners=[myeloid,CD8] infiltrating=[myeloid,CD8] +# +# Any model's cache works: only cell types, positions and the cell graph are used. +defaults: + - _self_ + - paths: default + - hydra: default + +task_name: spatial_stats +cells_dir: ??? +output_dir: ${paths.output_dir} +regions: null # list / .json / .txt of region ids; null = all cached regions +labels: + label_file: ??? + id_column: REGION_ID + label_column: ??? + patient_column: null +positive_name: positive +analyses: [nhood, cn, contacts, crossk] +# Optional fine -> coarse cell-type grouping ({fine: group}); identity by default. +group_map: {} +reference: [] # reference group(s) for contacts and cross-K (e.g. [tumor]); empty skips both +partners: null # partner groups (null = every other group) +infiltrating: [] # groups whose "intra-reference" fraction is reported +nhood: + n_perms: 200 + backend: numpy # numpy | squidpy +cn: + window_k: 10 + n_cn: 10 + sample_size: 200000 +crossk: + radii: [20, 50, 100] +seed: 0 +min_per_group: 10 # patients per class needed for a test diff --git a/src/grass_mil/configs/task/finetune_mil.yaml b/src/grass_mil/configs/task/finetune_mil.yaml index e4001c1..b351c60 100644 --- a/src/grass_mil/configs/task/finetune_mil.yaml +++ b/src/grass_mil/configs/task/finetune_mil.yaml @@ -3,20 +3,56 @@ task: name: finetune_mil aggregation: mil_attention - bag_key: patch_id + # One bag per region: with `data.sampler.runtime.unit_as_bag: true` every + # batch holds all sampled ego-graphs of one region, so the attention softmax + # and the loss cover the whole region. + bag_key: region_id bag_fallback_key: sample_id max_instances_per_bag: 0 instance_sampling: all - # The published MIL head is per-class: the bag logit for class c is + # The MIL head is per-class: the bag logit for class c is # sum_i A[i,c] * l[i,c], which requires one logit and one attention channel # per class. A single-logit BCE head cannot express the attribution identity, # so this preset is categorical with C=2 for binary tasks. target_type: categorical target_columns: null loss: categorical_ce + + # Class weights for the region cross-entropy: `auto_train` = inverse frequency + # on the TRAINING regions (mean 1, preprint); a list of floats; or null. + class_weights: auto_train + + # Per-component learning-rate multipliers on optim.lr. A scale of 0 freezes the + # component (no gradients, encoder kept in eval mode). `head_weight_decay` + # overrides optim.weight_decay for the head group (null = same as optimizer). optimization: + encoder_lr_scale: 1.0 + head_lr_scale: 1.0 + attention_lr_scale: 2.0 + head_weight_decay: null + # Legacy absolute pair (set both or neither); ignored when scales are used. backbone_lr: null attention_lr: null + + # Training-time augmentation of each ego-graph batch (preprint regularised + # regime: 0.05 / 0.05). Edge drop keeps both directions together; feature drop + # is element-wise and also writes the unassigned cell-type token. + finetune_augmentation: + drop_edge_p: 0.0 + drop_feat_p: 0.0 + seed: 0 + + # Linear-head pre-fit (grass-mil-probe output) and head freezing, used by the + # linear_* regimes. Requires model.graph_head.num_layers=1. + head_init_path: null + freeze_head: false + + # Gradient-norm clipping applied inside the region-accumulation flush. + grad_clip_norm: 1.0 + # Scheduler stepping: `step` (per optimizer step, with scheduler=cosine_mil_step + # and T_max: auto this anneals over the whole run) or `epoch`. + scheduler_interval: step + lr_warmup: enabled: false warmup_steps: 0 @@ -24,9 +60,9 @@ task: # Hyperbatch has one meaning only: # number of regions accumulated before one optimizer/scheduler step. region_accumulation: - enabled: false + enabled: true mode: manual_region_buffer - hyperbatch_size: 8 + hyperbatch_size: 12 flush_on_epoch_end: true # Per-class attention and matching head width. Override both together when diff --git a/src/grass_mil/configs/task/pretrain_bgrl_a3.yaml b/src/grass_mil/configs/task/pretrain_bgrl_a3.yaml new file mode 100644 index 0000000..103c661 --- /dev/null +++ b/src/grass_mil/configs/task/pretrain_bgrl_a3.yaml @@ -0,0 +1,53 @@ +# @package _global_ +# BGRL pretraining, preprint "A3" recipe: AdamW 5e-5 / weight decay 1e-5, +# linear LR warmup over 1000 steps then cosine to 1e-7 at 200k steps, target +# EMA momentum 0.99 -> 1.0 (cosine, no warmup), symmetric augmentations +# DropImportance(mu 0.25, p_lambda 0.45, degree importance, unassigned token) +# + Gaussian noise (std 0.2) on the cell-size feature, checkpoint every 5000 +# steps, all regions used label-free. +defaults: + - override /scheduler: cosine_step + - override /optim: adamw + +optim: + lr: 5.0e-5 + weight_decay: 1.0e-5 + +scheduler: + T_max: auto # = total_steps - lr warmup + eta_min: 1.0e-7 + +task: + name: pretrain_bgrl + aggregation: mean + bag_key: patch_id + bag_fallback_key: sample_id + max_instances_per_bag: 0 + instance_sampling: all + target_type: binary + target_columns: null + loss: categorical_bce + lr_warmup: + enabled: true + warmup_steps: 1000 + start_factor: 0.0 # linear from ~0 + warmup_steps: 1000 + total_steps: 200000 + momentum: 0.99 + momentum_final: 1.0 + momentum_warmup_steps: 0 + augmentation_seed: 0 + augmentation: + mode: importance + mu: 0.25 + p_lambda: 0.45 + feature_noise_std: 0.2 + # Continuous node feature(s) to noise, by name (molecular_columns) or index. + # Set to [] for cohorts without a cell-size feature. + feature_noise_columns: [SIZE] + +trainer: + max_steps: ${task.total_steps} + max_epochs: -1 + precision: 16-mixed + val_check_interval: null diff --git a/src/grass_mil/configs/train.yaml b/src/grass_mil/configs/train.yaml index 9d7886d..f1e320d 100644 --- a/src/grass_mil/configs/train.yaml +++ b/src/grass_mil/configs/train.yaml @@ -23,6 +23,9 @@ defaults: # experiment configs allow for version control of specific hyperparameters # e.g. best hyperparameters for given model and datamodule - experiment: null + # Training regime presets (configs/regime/*.yaml): mil, mil_frozen, + # linear_probehead, linear_ftall, linear_ftall_reg. Select with `regime=mil`. + - regime: null # config for hyperparameter optimization - hparams_search: null diff --git a/src/grass_mil/configs/trainer/gpu_amp.yaml b/src/grass_mil/configs/trainer/gpu_amp.yaml new file mode 100644 index 0000000..6aef739 --- /dev/null +++ b/src/grass_mil/configs/trainer/gpu_amp.yaml @@ -0,0 +1,6 @@ +defaults: + - default + +accelerator: gpu +devices: 1 +precision: 16-mixed diff --git a/src/grass_mil/data/components/datasets.py b/src/grass_mil/data/components/datasets.py index d6c5f23..a755eeb 100644 --- a/src/grass_mil/data/components/datasets.py +++ b/src/grass_mil/data/components/datasets.py @@ -16,6 +16,8 @@ class ProcessedIndexEntry: region_id: Optional[str] patch_id: str split: Optional[str] = None + patient_id: Optional[str] = None + graph_y: Optional[List[float]] = None def _migrate_legacy_attributes(data): diff --git a/src/grass_mil/data/components/graph_builders.py b/src/grass_mil/data/components/graph_builders.py index ad345c2..ea71cf6 100644 --- a/src/grass_mil/data/components/graph_builders.py +++ b/src/grass_mil/data/components/graph_builders.py @@ -59,13 +59,33 @@ def get_graph_builder(config: GraphBuilderConfig) -> GraphBuilder: @register_graph_builder("delaunay") class DelaunayGraphBuilder(GraphBuilder): + """Delaunay triangulation over cell centroids. + + Edge features (``edge_features``): + + - ``distance``: raw Euclidean distance in coordinate units. + - ``log_distance``: ``clip((ln(d + eps) - log_lower) / (log_upper - log_lower), 0, 1)``, + the bounded log-distance used as the GINE edge input in the GRASS-MIL preprint + (defaults ``log_lower=2.0``, ``log_upper=5.0``; with coordinates in pixels this maps + ~7 px -> 0 and ~148 px -> 1). + - ``neighbor``: 1 if ``distance <= neighbor_cutoff_um`` else 0. + """ + def __init__( self, edge_features: Optional[List[str]] = None, neighbor_cutoff_um: Optional[float] = None, + log_lower: float = 2.0, + log_upper: float = 5.0, + log_eps: float = 1e-5, ) -> None: self.edge_features = edge_features or [] self.neighbor_cutoff_um = neighbor_cutoff_um + if log_upper <= log_lower: + raise ValueError("log_upper must be greater than log_lower") + self.log_lower = float(log_lower) + self.log_upper = float(log_upper) + self.log_eps = float(log_eps) def build(self, coords: np.ndarray) -> GraphBuildResult: if coords.shape[0] < 2: @@ -74,7 +94,13 @@ def build(self, coords: np.ndarray) -> GraphBuildResult: edges = {(0, 1), (1, 0)} if coords.shape[0] == 2 else set() edge_index = _edges_to_tensor(edges) edge_attr, edge_attr_names = _build_edge_attr( - edge_index, coords, self.edge_features, self.neighbor_cutoff_um + edge_index, + coords, + self.edge_features, + self.neighbor_cutoff_um, + log_lower=self.log_lower, + log_upper=self.log_upper, + log_eps=self.log_eps, ) return GraphBuildResult( edge_index=edge_index, @@ -95,7 +121,13 @@ def build(self, coords: np.ndarray) -> GraphBuildResult: edges.add((b, a)) edge_index = _edges_to_tensor(edges) edge_attr, edge_attr_names = _build_edge_attr( - edge_index, coords, self.edge_features, self.neighbor_cutoff_um + edge_index, + coords, + self.edge_features, + self.neighbor_cutoff_um, + log_lower=self.log_lower, + log_upper=self.log_upper, + log_eps=self.log_eps, ) return GraphBuildResult( edge_index=edge_index, edge_attr=edge_attr, edge_attr_names=edge_attr_names @@ -109,11 +141,22 @@ def _edges_to_tensor(edges: set[tuple[int, int]]) -> torch.Tensor: return edge_index +def log_distance_feature( + distances: np.ndarray, log_lower: float = 2.0, log_upper: float = 5.0, eps: float = 1e-5 +) -> np.ndarray: + """Bounded log-distance edge feature: clip((ln(d + eps) - lower) / (upper - lower), 0, 1).""" + log_d = np.log(np.asarray(distances, dtype=np.float64) + eps) + return np.clip((log_d - log_lower) / (log_upper - log_lower), 0.0, 1.0).astype(np.float32) + + def _build_edge_attr( edge_index: torch.Tensor, coords: np.ndarray, edge_features: List[str], neighbor_cutoff_um: Optional[float], + log_lower: float = 2.0, + log_upper: float = 5.0, + log_eps: float = 1e-5, ) -> tuple[Optional[torch.Tensor], Optional[List[str]]]: if not edge_features: return None, None @@ -131,6 +174,8 @@ def _build_edge_attr( for feat in edge_features: if feat == "distance": columns.append(distances.astype(np.float32)) + elif feat == "log_distance": + columns.append(log_distance_feature(distances, log_lower, log_upper, log_eps)) elif feat == "neighbor": if neighbor_cutoff_um is None: raise ValueError( diff --git a/src/grass_mil/data/components/merge.py b/src/grass_mil/data/components/merge.py new file mode 100644 index 0000000..e146633 --- /dev/null +++ b/src/grass_mil/data/components/merge.py @@ -0,0 +1,79 @@ +"""Merge several graph units into one disconnected graph. + +Used by the pretraining sampler (``runtime.merge_units``: ego-graph roots are +drawn from the union of all regions, as the preprint's BGRL does) and by the +patient-level "merged bag" scoring, where the cores of one patient are pooled +into a single bag. +""" + +from __future__ import annotations + +from typing import Any, Dict, List, Optional, Sequence + +import torch +from torch_geometric.data import Data + +__all__ = ["merge_graph_units"] + +_NODE_KEYS = ("x", "pos", "x_raw", "categorical_codes") +_EDGE_KEYS = ("edge_attr",) +_COPY_FIRST = ("edge_attr_names", "categorical_labels", "categorical_slices", "molecular_feature_names") + + +def merge_graph_units( + units: Sequence[Data], + *, + keep_graph_attrs: Optional[Sequence[str]] = ("graph_y", "graph_w"), +) -> Data: + """Concatenate node and edge tensors with offset ``edge_index``. + + The merged graph carries ``unit_index`` (node -> position in ``units``), + ``unit_region_ids`` / ``unit_sample_ids`` / ``unit_patch_ids`` lists and + ``unit_ptr`` (node offsets). Graph-level attributes listed in + ``keep_graph_attrs`` are stacked per unit when present on every unit. + """ + if len(units) == 0: + raise ValueError("merge_graph_units needs at least one unit.") + node_parts: Dict[str, List[torch.Tensor]] = {} + edge_parts: Dict[str, List[torch.Tensor]] = {} + edge_index_parts: List[torch.Tensor] = [] + unit_index: List[torch.Tensor] = [] + offsets = [0] + for i, unit in enumerate(units): + n = int(unit.num_nodes) + for key in _NODE_KEYS: + val = getattr(unit, key, None) + if isinstance(val, torch.Tensor): + node_parts.setdefault(key, []).append(val) + for key in _EDGE_KEYS: + val = getattr(unit, key, None) + if isinstance(val, torch.Tensor): + edge_parts.setdefault(key, []).append(val) + ei = unit.edge_index + edge_index_parts.append(ei + offsets[-1] if ei is not None else torch.zeros(2, 0, dtype=torch.long)) + unit_index.append(torch.full((n,), i, dtype=torch.long)) + offsets.append(offsets[-1] + n) + merged = Data(edge_index=torch.cat(edge_index_parts, dim=1)) + for key, parts in node_parts.items(): + if len(parts) != len(units): + raise ValueError(f"Node attribute '{key}' is missing on some units; cannot merge.") + setattr(merged, key, torch.cat(parts, dim=0)) + for key, parts in edge_parts.items(): + if len(parts) != len(units): + raise ValueError(f"Edge attribute '{key}' is missing on some units; cannot merge.") + setattr(merged, key, torch.cat(parts, dim=0)) + merged.num_nodes = offsets[-1] + merged.unit_index = torch.cat(unit_index) + merged.unit_ptr = torch.tensor(offsets, dtype=torch.long) + merged.unit_region_ids = [None if getattr(u, "region_id", None) is None else str(u.region_id) for u in units] + merged.unit_sample_ids = [None if getattr(u, "sample_id", None) is None else str(u.sample_id) for u in units] + merged.unit_patch_ids = [None if getattr(u, "patch_id", None) is None else str(u.patch_id) for u in units] + for key in _COPY_FIRST: + val = getattr(units[0], key, None) + if val is not None: + setattr(merged, key, val) + for key in keep_graph_attrs or (): + vals = [getattr(u, key, None) for u in units] + if all(isinstance(v, torch.Tensor) for v in vals): + setattr(merged, f"unit_{key}", torch.cat([v.view(1, -1) for v in vals], dim=0)) + return merged diff --git a/src/grass_mil/data/components/precompute.py b/src/grass_mil/data/components/precompute.py index 41285ac..3138c34 100644 --- a/src/grass_mil/data/components/precompute.py +++ b/src/grass_mil/data/components/precompute.py @@ -49,6 +49,10 @@ class GraphLabelConfig: id_column: str = "id" tasks: Optional[Sequence[str]] = None scope: str = "region" # region | patch | sample + # Optional grouping column (patient / donor). It is never used as a target; + # the value is attached as ``data.patient_id`` and recorded in the processed + # index so patient-grouped splits can be built without loading graphs. + patient_column: Optional[str] = None @classmethod def from_dict(cls, data: Optional[Dict[str, object]]) -> "GraphLabelConfig": @@ -244,6 +248,10 @@ def precompute(self) -> Path: "region_id": unit.region_id, "patch_id": unit.patch_id, "split": unit.split, + "patient_id": getattr(data, "patient_id", None), + "graph_y": ( + data.graph_y.view(-1).tolist() if hasattr(data, "graph_y") else None + ), } ) @@ -626,6 +634,15 @@ def _build_pyg_data( and table.molecular_features is not None ): data.x_raw = torch.from_numpy(table.molecular_features[indices]).float() + if ( + embedded is not None + and table.molecular_feature_names is not None + and self.feature_reducer_config.name == "identity" + and len(table.molecular_feature_names) == int(embedded.shape[1]) + ): + # Column names of `x` (identity reducer only), so augmentations can + # address continuous features by name (e.g. feature_noise_columns: [SIZE]). + data.molecular_feature_names = list(map(str, table.molecular_feature_names)) self._attach_categorical_indices( data, table.categorical_labels, label_maps, indices @@ -640,7 +657,17 @@ def _build_pyg_data( ) if label_id in graph_label_df.index: row = graph_label_df.loc[label_id] - tasks = self.graph_label_config.tasks or list(row.index) + patient_column = self.graph_label_config.patient_column + tasks = self.graph_label_config.tasks or [ + c for c in row.index if c != patient_column + ] + if patient_column: + if patient_column not in row.index: + raise ValueError( + f"graph_labels.patient_column '{patient_column}' not found in " + f"{self.graph_label_config.label_file}" + ) + data.patient_id = str(row[patient_column]) graph_values = [] for task in tasks: value = row[task] diff --git a/src/grass_mil/data/components/samplers.py b/src/grass_mil/data/components/samplers.py index f5dd1ea..72fb2b5 100644 --- a/src/grass_mil/data/components/samplers.py +++ b/src/grass_mil/data/components/samplers.py @@ -68,12 +68,40 @@ class RuntimeShadowConfig: min_weight: float = 1e-6 subsample_fraction: float = 1.0 subsample_seed: Optional[int] = None + # Absolute number of root cells drawn per unit per epoch (uniformly, without + # replacement). Overrides `subsample_fraction` when set; `null` keeps every + # candidate root. The preprint trains on 256 (fine-tuning) / 512 (frozen + # encoder) roots per region per step and validates on a fixed 1024. + subsample_count: Optional[int] = None + # Seed that makes the per-unit root subset identical on every pass (the + # generator is re-seeded from `seed` and the unit index). Use for validation + # so epoch-to-epoch metric differences reflect the model, not the sample. + fixed_per_unit_seed: Optional[int] = None + # One batch per unit holding all of its selected roots, so the MIL bag, the + # attention softmax and the loss all cover the whole region at once. + # `subgraph_batch_size` is then only an upper bound (chunks are concatenated). + unit_as_bag: bool = False + # Pretraining: sample roots from the union of all units rather than one unit + # at a time (the preprint's BGRL draws its 64-root batches this way). + merge_units: bool = False interior_seeds: InteriorSeedConfig = field(default_factory=InteriorSeedConfig) ego_radius: EgoRadiusConfig = field(default_factory=EgoRadiusConfig) @classmethod def from_dict(cls, cfg: Optional[Dict[str, Any]]) -> "RuntimeShadowConfig": cfg = cfg or {} + num_neighbors = int(cfg.get("num_neighbors", 8)) + if num_neighbors < 0: + # -1 = unlimited: every neighbour is kept at every hop (full k-hop ego-graph). + num_neighbors = UNLIMITED_NEIGHBORS + subsample_count = cfg.get("subsample_count") + if subsample_count is not None: + subsample_count = int(subsample_count) + if subsample_count <= 0: + raise ValueError("runtime.subsample_count must be a positive integer or null.") + fixed_per_unit_seed = cfg.get("fixed_per_unit_seed") + if fixed_per_unit_seed is not None: + fixed_per_unit_seed = int(fixed_per_unit_seed) weight_mode = str(cfg.get("weight_mode", "inverse")).strip().lower() allowed_weight_modes = {"inverse", "sqrt_inverse", "proportional"} if weight_mode not in allowed_weight_modes: @@ -100,7 +128,7 @@ def from_dict(cls, cfg: Optional[Dict[str, Any]]) -> "RuntimeShadowConfig": return cls( enabled=bool(cfg.get("enabled", False)), depth=int(cfg.get("depth", 2)), - num_neighbors=int(cfg.get("num_neighbors", 8)), + num_neighbors=num_neighbors, subgraph_batch_size=int(cfg.get("subgraph_batch_size", 32)), replace=bool(cfg.get("replace", False)), shuffle_subgraphs=bool(cfg.get("shuffle_subgraphs", True)), @@ -113,11 +141,18 @@ def from_dict(cls, cfg: Optional[Dict[str, Any]]) -> "RuntimeShadowConfig": min_weight=min_weight, subsample_fraction=subsample_fraction, subsample_seed=subsample_seed, + subsample_count=subsample_count, + fixed_per_unit_seed=fixed_per_unit_seed, + unit_as_bag=bool(cfg.get("unit_as_bag", False)), + merge_units=bool(cfg.get("merge_units", False)), interior_seeds=InteriorSeedConfig.from_dict(cfg.get("interior_seeds")), ego_radius=EgoRadiusConfig.from_dict(cfg.get("ego_radius")), ) +UNLIMITED_NEIGHBORS = 10**8 + + def _build_weighted_node_idx( data: Data, *, @@ -275,14 +310,25 @@ def _subsample_count(num_candidates: int, subsample_fraction: float) -> int: return min(n_keep, num_candidates) +def _resolve_num_selected_roots( + num_candidates: int, *, subsample_fraction: float, subsample_count: Optional[int] +) -> int: + if subsample_count is not None: + return min(int(subsample_count), int(num_candidates)) + return _subsample_count(num_candidates, subsample_fraction) + + def _subsample_root_candidates( candidates: torch.Tensor, *, subsample_fraction: float, generator: Optional[torch.Generator], + subsample_count: Optional[int] = None, ) -> torch.Tensor: n_candidates = int(candidates.numel()) - n_keep = _subsample_count(n_candidates, subsample_fraction) + n_keep = _resolve_num_selected_roots( + n_candidates, subsample_fraction=subsample_fraction, subsample_count=subsample_count + ) if n_keep >= n_candidates: return candidates perm = torch.randperm(n_candidates, generator=generator) @@ -290,6 +336,15 @@ def _subsample_root_candidates( return candidates.index_select(0, selected) +def _per_unit_generator(seed: Optional[int], unit_index: int) -> Optional[torch.Generator]: + """Generator that yields the same root subset for a unit on every pass.""" + if seed is None: + return None + gen = torch.Generator() + gen.manual_seed(int(seed) * 1000003 + int(unit_index)) + return gen + + class BaseSamplerStrategy: """Data-layer agnostic sampler strategy. @@ -345,6 +400,13 @@ def __len__(self) -> int: if subgraph_batch_size <= 0: subgraph_batch_size = max(1, int(self.dataset_batch_size)) + if self.runtime.merge_units: + total_roots = sum( + self._num_selected_roots_for_unit(self.dataset[idx]) + for idx in range(len(self.dataset)) + ) + return int((total_roots + subgraph_batch_size - 1) // subgraph_batch_size) + total_batches = 0 for idx in range(len(self.dataset)): unit_data = self.dataset[idx] @@ -365,16 +427,107 @@ def __len__(self) -> int: if computed_node_idx is not None: effective_node_idx = computed_node_idx num_candidate_roots = _resolve_num_roots(unit_data, effective_node_idx) - num_roots = _subsample_count( - num_candidate_roots, float(self.runtime.subsample_fraction) + num_roots = _resolve_num_selected_roots( + num_candidate_roots, + subsample_fraction=float(self.runtime.subsample_fraction), + subsample_count=self.runtime.subsample_count, ) - total_batches += int((num_roots + subgraph_batch_size - 1) // subgraph_batch_size) + if self.runtime.unit_as_bag: + total_batches += 1 if num_roots > 0 else 0 + else: + total_batches += int( + (num_roots + subgraph_batch_size - 1) // subgraph_batch_size + ) return total_batches + def _num_selected_roots_for_unit(self, unit_data: Data) -> int: + effective_node_idx = self.runtime.node_idx + interior_idx = _resolve_interior_indices(unit_data, self.runtime, self._interior_cache) + if interior_idx is not None: + effective_node_idx = interior_idx + num_candidate_roots = _resolve_num_roots(unit_data, effective_node_idx) + return _resolve_num_selected_roots( + num_candidate_roots, + subsample_fraction=float(self.runtime.subsample_fraction), + subsample_count=self.runtime.subsample_count, + ) + + def _iter_merged(self, subgraph_batch_size: int): + """One pass over the roots of ALL units pooled into a single graph. + + The preprint's BGRL draws every 64-root batch from the merged + all-region graph, so a batch mixes ego-graphs of different regions. + Root selection per unit follows the same rules as the per-unit path + (interior seeds, subsample fraction/count, fixed seeds); weighted + (cell-type proportional) root sampling is not applied in merged mode. + """ + from grass_mil.data.components.merge import merge_graph_units + + units = [self.dataset[idx] for idx in range(len(self.dataset))] + merged = merge_graph_units(units) + roots: list[torch.Tensor] = [] + subsample_generator = None + if self.runtime.subsample_seed is not None: + subsample_generator = torch.Generator() + subsample_generator.manual_seed(int(self.runtime.subsample_seed)) + offsets = merged.unit_ptr + for idx, unit_data in enumerate(units): + effective_node_idx = self.runtime.node_idx + interior_idx = _resolve_interior_indices( + unit_data, self.runtime, self._interior_cache + ) + if interior_idx is not None: + effective_node_idx = interior_idx + candidates = _resolve_root_candidates(unit_data, effective_node_idx) + unit_generator = _per_unit_generator(self.runtime.fixed_per_unit_seed, idx) + candidates = _subsample_root_candidates( + candidates, + subsample_fraction=float(self.runtime.subsample_fraction), + generator=unit_generator if unit_generator is not None else subsample_generator, + subsample_count=self.runtime.subsample_count, + ) + if candidates.numel() == 0: + continue + roots.append(candidates.long() + int(offsets[idx])) + if not roots: + return + node_idx = torch.cat(roots) + if self.shuffle: + node_idx = node_idx[torch.randperm(node_idx.numel())] + unit_loader_kwargs: Dict[str, Any] = { + "num_workers": self.num_workers, + "pin_memory": self.pin_memory, + "persistent_workers": self.kwargs.get("persistent_workers", False) + if self.num_workers > 0 + else False, + } + if self.num_workers > 0 and "prefetch_factor" in self.kwargs: + unit_loader_kwargs["prefetch_factor"] = self.kwargs["prefetch_factor"] + loader = self.strategy.build_unit_loader( + data=merged, + depth=int(self.runtime.depth), + num_neighbors=int(self.runtime.num_neighbors), + batch_size=subgraph_batch_size, + node_idx=node_idx, + replace=bool(self.runtime.replace), + shuffle=False, + transform=_compose_ego_radius_transform(self.kwargs.get("transform"), self.runtime), + **unit_loader_kwargs, + ) + for sub_batch in loader: + yield _normalize_shadow_batch(sub_batch, merged) + def __iter__(self): if len(self.dataset) == 0: return + if self.runtime.merge_units: + subgraph_batch_size = int(self.runtime.subgraph_batch_size) + if subgraph_batch_size <= 0: + subgraph_batch_size = max(1, int(self.dataset_batch_size)) + yield from self._iter_merged(subgraph_batch_size) + return + indices = torch.arange(len(self.dataset), dtype=torch.long) if self.shuffle: indices = indices[torch.randperm(len(indices))] @@ -408,11 +561,18 @@ def __iter__(self): effective_root_candidates = _resolve_root_candidates( unit_data, effective_node_idx ) + unit_generator = _per_unit_generator(self.runtime.fixed_per_unit_seed, idx) effective_root_candidates = _subsample_root_candidates( effective_root_candidates, subsample_fraction=float(self.runtime.subsample_fraction), - generator=subsample_generator, + generator=unit_generator if unit_generator is not None else subsample_generator, + subsample_count=self.runtime.subsample_count, ) + if effective_root_candidates.numel() == 0: + continue + unit_batch_size = subgraph_batch_size + if self.runtime.unit_as_bag: + unit_batch_size = int(effective_root_candidates.numel()) unit_loader_kwargs: Dict[str, Any] = { "num_workers": self.num_workers, "pin_memory": self.pin_memory, @@ -426,7 +586,7 @@ def __iter__(self): data=unit_data, depth=int(self.runtime.depth), num_neighbors=int(self.runtime.num_neighbors), - batch_size=subgraph_batch_size, + batch_size=unit_batch_size, node_idx=effective_root_candidates, replace=bool(self.runtime.replace), shuffle=bool(self.runtime.shuffle_subgraphs and self.shuffle), diff --git a/src/grass_mil/data/components/splits.py b/src/grass_mil/data/components/splits.py new file mode 100644 index 0000000..5459e53 --- /dev/null +++ b/src/grass_mil/data/components/splits.py @@ -0,0 +1,197 @@ +"""Explicit splits, fold files and patient-grouped inner folds. + +The preprint's cross-validation uses fixed, patient-grouped outer folds stored +as a JSON list of region-id lists, and patient-grouped stratified inner folds +built from the development regions of each outer fold. These helpers read and +build those splits; the datamodule applies them through ``data.split.explicit``. +""" + +from __future__ import annotations + +import json +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple + +import numpy as np +import pandas as pd + +__all__ = [ + "ExplicitSplit", + "LabelTable", + "load_fold_json", + "load_labels", + "make_inner_folds", + "write_fold_json", +] + + +@dataclass +class ExplicitSplit: + """Region (or sample) ids per split; ids not listed are excluded.""" + + train: List[str] = field(default_factory=list) + val: List[str] = field(default_factory=list) + test: List[str] = field(default_factory=list) + id_kind: str = "region" # region | sample | patch + + @classmethod + def from_dict(cls, cfg: Optional[Mapping[str, Any]]) -> Optional["ExplicitSplit"]: + if not cfg: + return None + train = cfg.get("train") + val = cfg.get("val") + test = cfg.get("test") + if train is None and val is None and test is None: + return None + id_kind = str(cfg.get("id_kind", "region")) + if id_kind not in {"region", "sample", "patch"}: + raise ValueError("split.explicit.id_kind must be 'region', 'sample' or 'patch'.") + + def _ids(value: Any) -> List[str]: + if value is None: + return [] + if isinstance(value, (str, Path)): + path = Path(str(value)) + if path.suffix.lower() == ".json": + return [str(v) for v in json.loads(path.read_text())] + return [line.strip() for line in path.read_text().splitlines() if line.strip()] + return [str(v) for v in value] + + split = cls(train=_ids(train), val=_ids(val), test=_ids(test), id_kind=id_kind) + overlap = (set(split.train) & set(split.val)) | (set(split.train) & set(split.test)) + overlap |= set(split.val) & set(split.test) + if overlap and not (set(split.val) == set(split.test) and not (set(split.train) & overlap)): + # val == test is tolerated (refit runs score the held-out fold as "val" + # without using it for selection); any other overlap is a leak. + raise ValueError(f"split.explicit lists overlap: {sorted(overlap)[:10]}") + return split + + def lookup(self) -> Dict[str, str]: + out: Dict[str, str] = {} + for name in ("train", "val", "test"): + for rid in getattr(self, name): + out.setdefault(rid, name) + return out + + +def load_fold_json(path: str | Path) -> List[List[str]]: + """Outer folds: a JSON list of lists of region ids.""" + folds = json.loads(Path(path).read_text()) + if not isinstance(folds, list) or not all(isinstance(f, list) for f in folds): + raise ValueError(f"{path} must contain a JSON list of lists of region ids.") + folds = [[str(r) for r in fold] for fold in folds] + seen: set[str] = set() + for fold in folds: + dup = seen & set(fold) + if dup: + raise ValueError(f"Region ids appear in more than one fold: {sorted(dup)[:10]}") + seen |= set(fold) + return folds + + +def write_fold_json(folds: Sequence[Sequence[str]], path: str | Path) -> None: + Path(path).write_text(json.dumps([[str(r) for r in f] for f in folds])) + + +@dataclass +class LabelTable: + """Per-region label and grouping information.""" + + labels: pd.Series # index: region id (str) -> int label + groups: pd.Series # index: region id (str) -> group id (patient), str + + def label_of(self, rid: str) -> int: + return int(self.labels.loc[str(rid)]) + + def group_of(self, rid: str) -> str: + return str(self.groups.loc[str(rid)]) + + def subset(self, ids: Sequence[str]) -> "LabelTable": + ids = [str(r) for r in ids] + return LabelTable(labels=self.labels.loc[ids], groups=self.groups.loc[ids]) + + +def load_labels( + label_file: str | Path, + *, + id_column: str, + label_column: str, + patient_column: Optional[str] = None, + drop_unlabelled: bool = True, +) -> LabelTable: + """Read a label CSV into region -> label and region -> group series. + + Without ``patient_column`` every region is its own group (no grouping). + Non-numeric labels are mapped to codes in sorted order. + """ + df = pd.read_csv(label_file) + df.columns = [str(c).strip() for c in df.columns] + for col in (id_column, label_column) + ((patient_column,) if patient_column else ()): + if col not in df.columns: + raise ValueError(f"Column '{col}' not found in {label_file}; columns: {list(df.columns)}") + df[id_column] = df[id_column].astype(str) + df = df.drop_duplicates(id_column).set_index(id_column) + values = df[label_column] + if drop_unlabelled: + values = values.dropna() + if not np.issubdtype(values.dtype, np.number): + codes = {v: i for i, v in enumerate(sorted(values.astype(str).unique()))} + values = values.astype(str).map(codes) + labels = values.astype(float).round().astype(int) + if patient_column: + groups = df.loc[labels.index, patient_column].astype(str) + else: + groups = pd.Series(labels.index, index=labels.index, dtype=str) + return LabelTable(labels=labels, groups=groups) + + +def make_inner_folds( + dev_ids: Sequence[str], + table: LabelTable, + *, + k: int = 3, + seed: int = 0, + max_reseeds: int = 20, +) -> List[Tuple[List[str], List[str]]]: + """Stratified, group-aware inner folds over the SORTED development ids. + + Uses ``StratifiedGroupKFold(k, shuffle=True, random_state=seed)``; sorting the + ids first makes the folds a deterministic function of the id set, which the + preprint relies on for reproducible epoch selection. With very few groups + sklearn's greedy assignment can leave a fold without one class, which makes + AUROC undefined; such a split is rejected and the next seed is tried + (``max_reseeds`` times) before giving up. Large cohorts keep ``seed``. + """ + from sklearn.model_selection import StratifiedGroupKFold + + ids = np.array(sorted(str(r) for r in dev_ids)) + y = np.array([table.label_of(r) for r in ids]) + g = np.array([table.group_of(r) for r in ids]) + n_classes = len(np.unique(y)) + if n_classes < 2: + raise ValueError("Inner folds need at least two classes among the development regions.") + if len(np.unique(g)) < int(k): + raise ValueError( + f"Inner folds need at least k={k} patient groups; got {len(np.unique(g))}." + ) + last_error: Optional[str] = None + for attempt in range(int(max_reseeds) + 1): + splitter = StratifiedGroupKFold( + n_splits=int(k), shuffle=True, random_state=int(seed) + attempt + ) + folds: List[Tuple[List[str], List[str]]] = [] + ok = True + for tr, va in splitter.split(np.zeros((len(ids), 1)), y, g): + if len(np.unique(y[tr])) < n_classes or len(np.unique(y[va])) < 2: + ok = False + break + folds.append((ids[tr].tolist(), ids[va].tolist())) + if ok and len(folds) == int(k): + return folds + last_error = f"seed {int(seed) + attempt} left a fold without one class" + raise ValueError( + f"Could not build {k} stratified patient-grouped inner folds with every class in " + f"every split after {max_reseeds + 1} seeds ({last_error}). Use fewer inner folds " + "or more development patients." + ) diff --git a/src/grass_mil/data/spatial_omics_datamodule.py b/src/grass_mil/data/spatial_omics_datamodule.py index d1551c0..ceb2434 100644 --- a/src/grass_mil/data/spatial_omics_datamodule.py +++ b/src/grass_mil/data/spatial_omics_datamodule.py @@ -11,6 +11,7 @@ from .components.graph_builders import GraphBuilderConfig from .components.loaders import CsvConfig, H5adConfig, SceConfig from .components.patching import TileConfig +from .components.splits import ExplicitSplit from .components.precompute import ( CategoricalFeatureConfig, GraphLabelConfig, @@ -60,6 +61,7 @@ def __init__( tiling: Dict[str, Any], sampler: Optional[Dict[str, Any]] = None, val_sampler: Optional[Dict[str, Any]] = None, + test_sampler: Optional[Dict[str, Any]] = None, manifest: Optional[Dict[str, Any]] = None, graph_labels: Optional[Dict[str, Any]] = None, transforms: Optional[list[Dict[str, Any]]] = None, @@ -82,6 +84,18 @@ def __init__( split_by=split["split_by"], loocv=LoocvConfig.from_dict(split.get("loocv")), ) + # Explicit id-list splits (nested CV): applied in setup() on top of the + # persisted precompute, without re-processing the graphs. + self.explicit_split = ExplicitSplit.from_dict(split.get("explicit")) + if self.explicit_split is not None: + reducer_name = str(feature_reducer.get("name", "identity")).strip().lower() + fit_mode = str(feature_reducer.get("fit_mode", "train_only")).strip().lower() + if reducer_name != "identity" and fit_mode == "train_only": + raise ValueError( + "data.split.explicit re-partitions an existing precompute, so a feature " + "reducer fitted on the persisted training split would leak into the new " + "test split. Use feature_reducer.name=identity or fit_mode=global." + ) self.csv_config = CsvConfig.from_dict(csv, use_molecular_features) self.h5ad_config = H5adConfig.from_dict(h5ad, use_molecular_features) self.sce_config = SceConfig.from_dict(sce, use_molecular_features) @@ -92,8 +106,17 @@ def __init__( self.val_sampler_config = ( SamplerConfig.from_dict(val_sampler) if val_sampler is not None else None ) + # Test/predict loader. `null` derives it from the training sampler with + # every root kept once (no subsampling, no proportional weighting, no + # unit-as-bag), so a held-out region is scored on all of its cells. + self.test_sampler_config = ( + SamplerConfig.from_dict(test_sampler) + if test_sampler is not None + else _all_roots_sampler_config(self.sampler_config) + ) self.sampler_strategy: Optional[BaseSamplerStrategy] = None self.val_sampler_strategy: Optional[BaseSamplerStrategy] = None + self.test_sampler_strategy: Optional[BaseSamplerStrategy] = None self.manifest_config = ManifestConfig.from_dict(manifest) self.graph_label_config = GraphLabelConfig.from_dict(graph_labels) self.transforms = instantiate_transforms(transforms) @@ -161,27 +184,26 @@ def setup(self, stage: Optional[str] = None) -> None: index_path = Path(self.processed_dir) / "processed_index.json" base_dataset = SpatialOmicsGraphDataset(index_path) - if not _entries_have_persisted_splits(base_dataset.entries): - raise ValueError( - "processed_index.json must contain valid per-entry split labels " - "('train', 'val', 'test'). Run data precompute/prepare_data() " - "to regenerate processed artifacts." - ) + if self.explicit_split is not None: + members = _apply_explicit_split(base_dataset, self.explicit_split) + else: + if not _entries_have_persisted_splits(base_dataset.entries): + raise ValueError( + "processed_index.json must contain valid per-entry split labels " + "('train', 'val', 'test'). Run data precompute/prepare_data() " + "to regenerate processed artifacts." + ) + members = { + name: [entry for entry in base_dataset.entries if entry.split == name] + for name in ("train", "val", "test") + } self.dataset_train = _subset_dataset_from_entries( - base_dataset, - [entry for entry in base_dataset.entries if entry.split == "train"], - self.transforms, - ) - self.dataset_val = _subset_dataset_from_entries( - base_dataset, - [entry for entry in base_dataset.entries if entry.split == "val"], - self.transforms, + base_dataset, members["train"], self.transforms ) + self.dataset_val = _subset_dataset_from_entries(base_dataset, members["val"], self.transforms) self.dataset_test = _subset_dataset_from_entries( - base_dataset, - [entry for entry in base_dataset.entries if entry.split == "test"], - self.transforms, + base_dataset, members["test"], self.transforms ) self._ensure_sampler_strategy() self._ensure_val_sampler_strategy() @@ -198,6 +220,13 @@ def _ensure_val_sampler_strategy(self) -> BaseSamplerStrategy: self.val_sampler_strategy = get_sampler_strategy(self.val_sampler_config) return self.val_sampler_strategy + def _ensure_test_sampler_strategy(self) -> BaseSamplerStrategy: + if self.test_sampler_config is None: + return self._ensure_sampler_strategy() + if self.test_sampler_strategy is None: + self.test_sampler_strategy = get_sampler_strategy(self.test_sampler_config) + return self.test_sampler_strategy + def train_dataloader(self) -> Any: sampler = self._ensure_sampler_strategy() return sampler.build_dataset_loader( @@ -219,7 +248,7 @@ def val_dataloader(self) -> Any: ) def test_dataloader(self) -> Any: - sampler = self._ensure_sampler_strategy() + sampler = self._ensure_test_sampler_strategy() return sampler.build_dataset_loader( dataset=self.dataset_test, batch_size=self.hparams.batch_size, @@ -240,6 +269,70 @@ def predict_dataloader(self) -> Any: return self.test_dataloader() +def unit_key(entry, id_kind: str) -> str: + """The id a processed entry is addressed by: region (default), sample or patch. + + ``patch`` makes every tile its own unit, which is how a cohort of a few + large images is cut into many bags for cross-validation. + """ + if id_kind == "patch": + return str(entry.patch_id) + if id_kind == "sample": + return str(entry.sample_id) + return str(entry.region_id if entry.region_id is not None else entry.sample_id) + + +def _apply_explicit_split( + dataset: SpatialOmicsGraphDataset, split: ExplicitSplit +) -> Dict[str, list]: + """Entries per split from explicit id lists; ids not listed are dropped. + + The same id may sit in ``val`` and ``test`` (refit runs log the held-out fold + as validation and score it as test), so membership is resolved per split + rather than by a single label on the entry. + """ + import dataclasses + + by_key: Dict[str, Any] = {} + for entry in dataset.entries: + key = str(unit_key(entry, split.id_kind)) + by_key.setdefault(key, []).append(entry) + wanted = {name: [str(r) for r in getattr(split, name)] for name in ("train", "val", "test")} + missing = sorted({r for ids in wanted.values() for r in ids} - set(by_key)) + if missing: + raise ValueError( + f"split.explicit names {len(missing)} {split.id_kind} id(s) with no processed " + f"graph (first: {missing[:10]}). Check the ids against processed_index.json." + ) + members: Dict[str, list] = {} + for name, ids in wanted.items(): + members[name] = [ + dataclasses.replace(entry, split=name) for rid in ids for entry in by_key[rid] + ] + dataset.entries = [e for group in members.values() for e in group] + return members + + +def _all_roots_sampler_config(train_cfg: SamplerConfig) -> Optional[SamplerConfig]: + """Evaluation variant of a sampler config: every root once, uniformly, no bagging.""" + if train_cfg is None or train_cfg.name == "identity": + return None + runtime = dict(train_cfg.runtime or {}) + runtime.update( + { + "subsample_fraction": 1.0, + "subsample_count": None, + "subsample_seed": None, + "fixed_per_unit_seed": None, + "proportional_root_sampling": False, + "unit_as_bag": False, + "merge_units": False, + "shuffle_subgraphs": False, + } + ) + return SamplerConfig(name=train_cfg.name, kwargs=dict(train_cfg.kwargs or {}), runtime=runtime) + + def _subset_dataset_from_entries( dataset: SpatialOmicsGraphDataset, selected_entries, diff --git a/src/grass_mil/evaluation/__init__.py b/src/grass_mil/evaluation/__init__.py new file mode 100644 index 0000000..31908c7 --- /dev/null +++ b/src/grass_mil/evaluation/__init__.py @@ -0,0 +1,39 @@ +"""Evaluation statistics for held-out scores (AUROC CIs, DeLong, operating points).""" + +from .operating_points import ( + confusion_counts, + flag_top_within_fold, + likelihood_ratios, + metrics_from_confusion, + operating_point, + operating_point_bootstrap, + ppv_npv_at_prevalence, + threshold_points, +) +from .stats import ( + auroc, + bootstrap_auroc_ci, + delong_test, + per_fold_auroc, + rank_within_fold, + stratified_auroc, + z_within_fold, +) + +__all__ = [ + "auroc", + "bootstrap_auroc_ci", + "confusion_counts", + "delong_test", + "flag_top_within_fold", + "likelihood_ratios", + "metrics_from_confusion", + "operating_point", + "operating_point_bootstrap", + "per_fold_auroc", + "ppv_npv_at_prevalence", + "rank_within_fold", + "stratified_auroc", + "threshold_points", + "z_within_fold", +] diff --git a/src/grass_mil/evaluation/cli.py b/src/grass_mil/evaluation/cli.py new file mode 100644 index 0000000..cdafff2 --- /dev/null +++ b/src/grass_mil/evaluation/cli.py @@ -0,0 +1,271 @@ +"""``grass-mil-scores``: AUROC CIs, DeLong comparisons and operating points.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Dict, List + +import hydra +import numpy as np +import pandas as pd +from omegaconf import DictConfig + +from grass_mil._paths import setup_project_root + +setup_project_root() + +from grass_mil.evaluation.operating_points import ( # noqa: E402 + flag_top_within_fold, + likelihood_ratios, + operating_point, + operating_point_bootstrap, + ppv_npv_at_prevalence, + threshold_points, +) +from grass_mil.evaluation.stats import ( # noqa: E402 + auroc, + bootstrap_auroc_ci, + delong_test, + per_fold_auroc, + rank_within_fold, + stratified_auroc, + z_within_fold, +) +from grass_mil.utils import RankedLogger # noqa: E402 + +log = RankedLogger(__name__, rank_zero_only=True) + +__all__ = ["run_scores", "main"] + + +def _prepare_table(cfg: DictConfig) -> pd.DataFrame: + df = pd.read_csv(str(cfg.input)) + df.columns = [str(c).strip() for c in df.columns] + label = str(cfg.label_column) + scores = [str(s) for s in cfg.scores] + fold = cfg.get("fold_column") + patient = cfg.get("patient_column") + strata = [str(s) for s in (cfg.get("strata") or [])] + for col in [label] + scores + ([str(fold)] if fold else []) + strata: + if col not in df.columns: + raise ValueError(f"Column '{col}' not in {cfg.input}; columns: {list(df.columns)}") + df = df.dropna(subset=[label] + scores) + if patient and str(patient) in df.columns and bool(cfg.get("patient_level", True)): + agg: Dict[str, Any] = {s: "mean" for s in scores} + agg[label] = "max" + if fold: + agg[str(fold)] = "first" + for s in strata: + agg[s] = "first" + agg["n_units"] = ("__one", "sum") + df = df.assign(__one=1) + grouped = df.groupby(str(patient)) + out = grouped.agg({k: v for k, v in agg.items() if k != "n_units"}) + out["n_units"] = grouped["__one"].sum() + df = out.reset_index() + df[label] = (df[label].astype(float) >= 0.5).astype(int) + return df + + +def _standardise(cfg: DictConfig, df: pd.DataFrame, col: str) -> np.ndarray: + how = str(cfg.get("standardise", "z")).lower() + fold = cfg.get("fold_column") + values = df[col].to_numpy(dtype=float) + if not fold or how == "none": + return values + folds = df[str(fold)].to_numpy() + if how == "z": + return z_within_fold(values, folds) + if how == "rank": + return rank_within_fold(values, folds) + raise ValueError("standardise must be z, rank or none") + + +def run_scores(cfg: DictConfig) -> Dict[str, Any]: + out_dir = Path(str(cfg.output_dir)) + out_dir.mkdir(parents=True, exist_ok=True) + df = _prepare_table(cfg) + label = str(cfg.label_column) + y = df[label].to_numpy(dtype=int) + fold = cfg.get("fold_column") + folds = df[str(fold)].to_numpy() if fold else None + n_boot = int(cfg.n_boot) + seed = int(cfg.seed) + unit = ( + "patients" + if cfg.get("patient_column") and bool(cfg.get("patient_level", True)) + else "units" + ) + prevalence = float(y.mean()) + md: List[str] = [ + f"# Held-out score evaluation ({len(y)} {unit}, {int(y.sum())} positive, prevalence {100 * prevalence:.0f} %)", + "", + f"Scores standardised within fold ({cfg.get('standardise', 'z')}) before pooling; " + f"{n_boot} bootstrap resamples, seed {seed}.", + "", + ] + + scores = [str(s) for s in cfg.scores] + std_scores = {s: _standardise(cfg, df, s) for s in scores} + summary_rows = [] + md += [ + "## AUROC", + "", + "| score | pooled AUROC [95% CI] | per-fold mean ± sd | raw pooled |", + "|---|---|---|---|", + ] + for s in scores: + ci = bootstrap_auroc_ci(y, std_scores[s], n_boot=n_boot, seed=seed) + row = { + "score": s, + "auroc": ci.auroc, + "lower": ci.lower, + "upper": ci.upper, + "raw_pooled_auroc": auroc(y, df[s].to_numpy(float)), + } + pf_txt = "–" + if folds is not None: + pf = per_fold_auroc(y, df[s].to_numpy(float), folds) + row.update({f"perfold_{k}": v for k, v in pf.items()}) + pf_txt = f"{pf['mean']:.3f} ± {pf['sd']:.3f} ({pf['n_folds']} folds)" + summary_rows.append(row) + md.append( + f"| {s} | {ci.auroc:.3f} [{ci.lower:.3f}, {ci.upper:.3f}] | {pf_txt} | {row['raw_pooled_auroc']:.3f} |" + ) + pd.DataFrame(summary_rows).to_csv(out_dir / "scores_summary.csv", index=False) + + delong_rows = [] + if len(scores) > 1: + md += [ + "", + "## DeLong comparisons (pooled, standardised scores)", + "", + "| score 1 | score 2 | ΔAUROC | z | p |", + "|---|---|---|---|---|", + ] + for i in range(len(scores)): + for j in range(i + 1, len(scores)): + d = delong_test(y, std_scores[scores[i]], std_scores[scores[j]]) + delong_rows.append( + { + "score_1": scores[i], + "score_2": scores[j], + "auroc_1": d.auroc_1, + "auroc_2": d.auroc_2, + "delta": d.delta, + "z": d.z, + "p_value": d.p_value, + } + ) + md.append( + f"| {scores[i]} | {scores[j]} | {d.delta:+.3f} | {d.z:.2f} | {d.p_value:.3g} |" + ) + pd.DataFrame(delong_rows).to_csv(out_dir / "delong.csv", index=False) + + strata_rows = [] + for col in [str(c) for c in (cfg.get("strata") or [])]: + md += [ + "", + f"## AUROC by {col} (first score: {scores[0]})", + "", + "| level | n | positives | AUROC [95% CI] |", + "|---|---|---|---|", + ] + for level, r in stratified_auroc( + y, std_scores[scores[0]], df[col].to_numpy(), n_boot=n_boot, seed=seed + ).items(): + strata_rows.append({"stratum": col, "level": level, **r}) + md.append( + f"| {level} | {r['n']} | {r['n_pos']} | {r['auroc']:.3f} [{r['lower']:.3f}, {r['upper']:.3f}] |" + ) + if strata_rows: + pd.DataFrame(strata_rows).to_csv(out_dir / "strata.csv", index=False) + + op_rows = [] + rules = [float(q) for q in cfg.operating_rules] + fold_key = folds if folds is not None else np.zeros(len(y), dtype=int) + for s in scores: + sc = std_scores[s] + md += [ + "", + f"## Operating points: {s}", + "", + "| rule | flagged | sensitivity | specificity | PPV | NPV | LR+ | LR− | TP/FP/FN/TN |", + "|---|---|---|---|---|---|---|---|---|", + ] + points = [ + (f"top {100 * q:.0f} % within fold", flag_top_within_fold(sc, fold_key, q), False) + for q in rules + ] + thr = threshold_points(sc, y) + for name, key in ( + ("Youden (all data, optimistic)", "youden"), + ("rule-out: sens ≥ 90 % (optimistic)", "rule_out"), + ("rule-in: spec ≥ 90 % (optimistic)", "rule_in"), + ): + t = thr[key] + if t == t: + points.append((name, (sc >= t).astype(int), True)) + for name, pred, optimistic in points: + m = operating_point(pred, y) + ci = operating_point_bootstrap(pred, y, n_boot=n_boot, seed=seed) + lr = likelihood_ratios(m["sensitivity"], m["specificity"]) + op_rows.append( + { + "score": s, + "rule": name, + "optimistic": optimistic, + **m, + **{f"{k}_lower": v[0] for k, v in ci.items()}, + **{f"{k}_upper": v[1] for k, v in ci.items()}, + **lr, + } + ) + md.append( + f"| {name} | {100 * m['flagged']:.0f} % | {100 * m['sensitivity']:.0f} % [{100 * ci['sensitivity'][0]:.0f}, {100 * ci['sensitivity'][1]:.0f}] | " + f"{100 * m['specificity']:.0f} % [{100 * ci['specificity'][0]:.0f}, {100 * ci['specificity'][1]:.0f}] | " + f"{100 * m['ppv']:.0f} % [{100 * ci['ppv'][0]:.0f}, {100 * ci['ppv'][1]:.0f}] | {100 * m['npv']:.0f} % [{100 * ci['npv'][0]:.0f}, {100 * ci['npv'][1]:.0f}] | " + f"{lr['lr_plus']:.2f} | {lr['lr_minus']:.2f} | {m['tp']}/{m['fp']}/{m['fn']}/{m['tn']} |" + ) + pd.DataFrame(op_rows).to_csv(out_dir / "operating_points.csv", index=False) + + prev_rows = [] + if rules: + s = scores[0] + q = rules[len(rules) // 2] + m = operating_point(flag_top_within_fold(std_scores[s], fold_key, q), y) + md += [ + "", + f"## PPV / NPV at other prevalences ({s}, top {100 * q:.0f} % rule: sensitivity {100 * m['sensitivity']:.0f} %, specificity {100 * m['specificity']:.0f} %)", + "", + "| prevalence | PPV | NPV |", + "|---|---|---|", + ] + for p in [float(v) for v in cfg.prevalences]: + r = ppv_npv_at_prevalence(m["sensitivity"], m["specificity"], p) + prev_rows.append({"score": s, "rule_q": q, **r}) + md.append(f"| {100 * p:.0f} % | {100 * r['ppv']:.0f} % | {100 * r['npv']:.0f} % |") + pd.DataFrame(prev_rows).to_csv(out_dir / "ppv_npv_by_prevalence.csv", index=False) + + report = out_dir / str(cfg.report_filename) + report.write_text("\n".join(md) + "\n") + summary = { + "n": int(len(y)), + "n_pos": int(y.sum()), + "scores": summary_rows, + "delong": delong_rows, + "report": str(report), + } + (out_dir / "scores_summary.json").write_text(json.dumps(summary, indent=1, default=float)) + log.info("\n".join(md[:12])) + return summary + + +@hydra.main(version_base="1.3", config_path="../configs", config_name="scores.yaml") +def main(cfg: DictConfig) -> None: + run_scores(cfg) + + +if __name__ == "__main__": + main() diff --git a/src/grass_mil/evaluation/operating_points.py b/src/grass_mil/evaluation/operating_points.py new file mode 100644 index 0000000..8c3034b --- /dev/null +++ b/src/grass_mil/evaluation/operating_points.py @@ -0,0 +1,136 @@ +"""Sensitivity / specificity / PPV / NPV at pre-specified operating rules. + +Thresholds are quantile rules applied *within fold* (flag the top fraction q +of units by score), so they do not depend on the fold models' score scales. +Threshold-free summaries (Youden, rule-in, rule-out) are chosen on all data +and reported as optimistic. +""" + +from __future__ import annotations + +from dataclasses import asdict, dataclass +from typing import Dict, List + +import numpy as np + +__all__ = [ + "Confusion", + "confusion_counts", + "flag_top_within_fold", + "likelihood_ratios", + "metrics_from_confusion", + "operating_point", + "operating_point_bootstrap", + "ppv_npv_at_prevalence", + "threshold_points", +] + + +@dataclass +class Confusion: + tp: int + fp: int + fn: int + tn: int + + +def confusion_counts(pred, y) -> Confusion: + pred = np.asarray(pred).astype(int).reshape(-1) + y = np.asarray(y).astype(int).reshape(-1) + return Confusion( + tp=int(((pred == 1) & (y == 1)).sum()), + fp=int(((pred == 1) & (y == 0)).sum()), + fn=int(((pred == 0) & (y == 1)).sum()), + tn=int(((pred == 0) & (y == 0)).sum()), + ) + + +def metrics_from_confusion(c: Confusion) -> Dict[str, float]: + n = c.tp + c.fp + c.fn + c.tn + return { + "sensitivity": c.tp / (c.tp + c.fn) if (c.tp + c.fn) else float("nan"), + "specificity": c.tn / (c.tn + c.fp) if (c.tn + c.fp) else float("nan"), + "ppv": c.tp / (c.tp + c.fp) if (c.tp + c.fp) else float("nan"), + "npv": c.tn / (c.tn + c.fn) if (c.tn + c.fn) else float("nan"), + "flagged": (c.tp + c.fp) / n if n else float("nan"), + "accuracy": (c.tp + c.tn) / n if n else float("nan"), + **asdict(c), + } + + +def flag_top_within_fold(score, folds, q: float) -> np.ndarray: + """Flag the top fraction ``q`` of units within each fold (threshold = fold quantile).""" + s = np.asarray(score, dtype=float).reshape(-1) + f = np.asarray(folds).reshape(-1) + pred = np.zeros(s.size, dtype=int) + for k in np.unique(f): + m = f == k + thr = np.quantile(s[m], 1.0 - float(q)) + pred[m] = (s[m] >= thr).astype(int) + return pred + + +def operating_point(pred, y) -> Dict[str, float]: + return metrics_from_confusion(confusion_counts(pred, y)) + + +def operating_point_bootstrap( + pred, y, *, n_boot: int = 2000, seed: int = 0, alpha: float = 0.05 +) -> Dict[str, List[float]]: + """Percentile bootstrap CIs over units for sensitivity, specificity, PPV, NPV.""" + pred = np.asarray(pred).astype(int).reshape(-1) + y = np.asarray(y).astype(int).reshape(-1) + rng = np.random.default_rng(seed) + keys = ("sensitivity", "specificity", "ppv", "npv") + draws = {k: [] for k in keys} + for _ in range(int(n_boot)): + idx = rng.integers(0, y.size, y.size) + m = operating_point(pred[idx], y[idx]) + for k in keys: + if m[k] == m[k]: + draws[k].append(m[k]) + return { + k: [float(v) for v in np.percentile(vals, [100 * alpha / 2, 100 * (1 - alpha / 2)])] + if vals + else [float("nan"), float("nan")] + for k, vals in draws.items() + } + + +def ppv_npv_at_prevalence( + sensitivity: float, specificity: float, prevalence: float +) -> Dict[str, float]: + p = float(prevalence) + ppv_den = sensitivity * p + (1 - specificity) * (1 - p) + npv_den = specificity * (1 - p) + (1 - sensitivity) * p + return { + "prevalence": p, + "ppv": sensitivity * p / ppv_den if ppv_den > 0 else float("nan"), + "npv": specificity * (1 - p) / npv_den if npv_den > 0 else float("nan"), + } + + +def likelihood_ratios(sensitivity: float, specificity: float) -> Dict[str, float]: + return { + "lr_plus": sensitivity / (1 - specificity) if specificity < 1 else float("inf"), + "lr_minus": (1 - sensitivity) / specificity if specificity > 0 else float("inf"), + } + + +def threshold_points( + score, y, *, rule_out_sens: float = 0.9, rule_in_spec: float = 0.9 +) -> Dict[str, float]: + """Youden-optimal, rule-out and rule-in thresholds chosen on ALL data (optimistic).""" + from sklearn.metrics import roc_curve + + s = np.asarray(score, dtype=float).reshape(-1) + yy = np.asarray(y).astype(int).reshape(-1) + fpr, tpr, thr = roc_curve(yy, s) + out: Dict[str, float] = {} + j = int(np.argmax(tpr - fpr)) + out["youden"] = float(thr[j]) + ro = np.where(tpr >= rule_out_sens)[0] + out["rule_out"] = float(thr[ro[0]]) if ro.size else float("nan") + ri = np.where(fpr <= 1 - rule_in_spec)[0] + out["rule_in"] = float(thr[ri[-1]]) if ri.size else float("nan") + return out diff --git a/src/grass_mil/evaluation/stats.py b/src/grass_mil/evaluation/stats.py new file mode 100644 index 0000000..8fe9782 --- /dev/null +++ b/src/grass_mil/evaluation/stats.py @@ -0,0 +1,187 @@ +"""AUROC statistics for held-out (out-of-fold) scores. + +Fold models of a cross-validation have different score scales, so pooled +AUROCs are computed on scores standardised within fold (z-score or rank); +per-fold means are reported alongside. The DeLong test uses the Sun & Xu +structural components for two correlated AUROCs on the same labels. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Dict, Tuple + +import numpy as np + +__all__ = [ + "AurocCI", + "DeLongResult", + "auroc", + "bootstrap_auroc_ci", + "delong_test", + "per_fold_auroc", + "rank_within_fold", + "stratified_auroc", + "z_within_fold", +] + + +def _arr(y, s) -> Tuple[np.ndarray, np.ndarray]: + y = np.asarray(y).astype(int).reshape(-1) + s = np.asarray(s, dtype=float).reshape(-1) + if y.shape[0] != s.shape[0]: + raise ValueError("labels and scores differ in length") + return y, s + + +def auroc(y, s) -> float: + """AUROC via the Mann-Whitney statistic (ties count one half); nan if one class.""" + y, s = _arr(y, s) + pos = s[y == 1] + neg = s[y == 0] + if pos.size == 0 or neg.size == 0: + return float("nan") + from scipy.stats import rankdata + + ranks = rankdata(np.concatenate([pos, neg])) + return float( + (ranks[: pos.size].sum() - pos.size * (pos.size + 1) / 2.0) / (pos.size * neg.size) + ) + + +@dataclass +class AurocCI: + auroc: float + lower: float + upper: float + n_boot: int + n_valid: int + + +def bootstrap_auroc_ci( + y, s, *, n_boot: int = 2000, seed: int = 0, alpha: float = 0.05, groups=None +) -> AurocCI: + """Percentile bootstrap CI over samples (or over ``groups``, e.g. patients).""" + y, s = _arr(y, s) + rng = np.random.default_rng(seed) + point = auroc(y, s) + vals = [] + if groups is not None: + groups = np.asarray(groups) + uniq = np.unique(groups) + members = {g: np.where(groups == g)[0] for g in uniq} + for _ in range(int(n_boot)): + pick = rng.integers(0, uniq.size, uniq.size) + idx = np.concatenate([members[uniq[i]] for i in pick]) + if 0 < y[idx].sum() < idx.size: + vals.append(auroc(y[idx], s[idx])) + else: + n = y.size + for _ in range(int(n_boot)): + idx = rng.integers(0, n, n) + if 0 < y[idx].sum() < n: + vals.append(auroc(y[idx], s[idx])) + if not vals: + return AurocCI(point, float("nan"), float("nan"), int(n_boot), 0) + lo, hi = np.percentile(vals, [100 * alpha / 2, 100 * (1 - alpha / 2)]) + return AurocCI(point, float(lo), float(hi), int(n_boot), len(vals)) + + +@dataclass +class DeLongResult: + auroc_1: float + auroc_2: float + delta: float + z: float + p_value: float + variance: float + + +def _structural_components(s: np.ndarray, pos: np.ndarray, neg: np.ndarray): + sp, sn = s[pos], s[neg] + v10 = np.array([(np.sum(sn < x) + 0.5 * np.sum(sn == x)) / sn.size for x in sp]) + v01 = np.array([(np.sum(sp > x) + 0.5 * np.sum(sp == x)) / sp.size for x in sn]) + return v10, v01 + + +def delong_test(y, s1, s2) -> DeLongResult: + """DeLong test for two correlated AUROCs on the same labels (Sun & Xu components).""" + from scipy import stats + + y, s1 = _arr(y, s1) + _, s2 = _arr(y, s2) + pos = np.where(y == 1)[0] + neg = np.where(y == 0)[0] + if pos.size < 2 or neg.size < 2: + raise ValueError("DeLong needs at least two positives and two negatives.") + a10, a01 = _structural_components(s1, pos, neg) + b10, b01 = _structural_components(s2, pos, neg) + auc1, auc2 = float(a10.mean()), float(b10.mean()) + s10 = np.cov(np.stack([a10, b10])) + s01 = np.cov(np.stack([a01, b01])) + S = s10 / pos.size + s01 / neg.size + var = float(S[0, 0] + S[1, 1] - 2 * S[0, 1]) + z = (auc1 - auc2) / np.sqrt(var) if var > 0 else 0.0 + p = float(2 * stats.norm.sf(abs(z))) + return DeLongResult(auc1, auc2, auc1 - auc2, float(z), p, var) + + +def z_within_fold(scores, folds) -> np.ndarray: + """Standardise scores to zero mean / unit sd within each fold.""" + s = np.asarray(scores, dtype=float).reshape(-1) + f = np.asarray(folds).reshape(-1) + out = np.zeros_like(s) + for k in np.unique(f): + m = f == k + sd = s[m].std() + out[m] = (s[m] - s[m].mean()) / (sd if sd > 0 else 1.0) + return out + + +def rank_within_fold(scores, folds) -> np.ndarray: + """Map scores to (rank - 0.5) / n within each fold (uniform on (0, 1)).""" + from scipy.stats import rankdata + + s = np.asarray(scores, dtype=float).reshape(-1) + f = np.asarray(folds).reshape(-1) + out = np.zeros_like(s) + for k in np.unique(f): + m = f == k + out[m] = (rankdata(s[m]) - 0.5) / m.sum() + return out + + +def per_fold_auroc(y, s, folds) -> Dict[str, float]: + """AUROC within each fold plus their mean and sd (the preprint's headline number).""" + y, s = _arr(y, s) + f = np.asarray(folds).reshape(-1) + vals = {} + for k in sorted(np.unique(f), key=str): + m = f == k + vals[str(k)] = auroc(y[m], s[m]) + arr = np.array([v for v in vals.values() if v == v]) + out = {f"fold_{k}": v for k, v in vals.items()} + out["mean"] = float(arr.mean()) if arr.size else float("nan") + out["sd"] = float(arr.std(ddof=1)) if arr.size > 1 else 0.0 + out["n_folds"] = int(arr.size) + return out + + +def stratified_auroc( + y, s, strata, *, n_boot: int = 2000, seed: int = 0 +) -> Dict[str, Dict[str, float]]: + """AUROC (with bootstrap CI) within each level of a stratifying variable.""" + y, s = _arr(y, s) + st = np.asarray(strata).reshape(-1) + out: Dict[str, Dict[str, float]] = {} + for level in sorted(np.unique(st), key=str): + m = st == level + ci = bootstrap_auroc_ci(y[m], s[m], n_boot=n_boot, seed=seed) + out[str(level)] = { + "n": int(m.sum()), + "n_pos": int(y[m].sum()), + "auroc": ci.auroc, + "lower": ci.lower, + "upper": ci.upper, + } + return out diff --git a/src/grass_mil/extract.py b/src/grass_mil/extract.py new file mode 100644 index 0000000..bf9743f --- /dev/null +++ b/src/grass_mil/extract.py @@ -0,0 +1,144 @@ +"""``grass-mil-extract``: per-cell cache of trained models (see inference/cell_cache.py).""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Dict, List, Optional + +import hydra +import torch +from omegaconf import DictConfig, open_dict + +from grass_mil._paths import setup_project_root + +setup_project_root() + +from grass_mil.data.components.datasets import SpatialOmicsGraphDataset # noqa: E402 +from grass_mil.inference.cell_cache import extract_cell_cache # noqa: E402 +from grass_mil.inference.model_loading import load_trained_module # noqa: E402 +from grass_mil.models.training.probe import probe_sampler_config # noqa: E402 +from grass_mil.utils import RankedLogger, extras # noqa: E402 + +log = RankedLogger(__name__, rank_zero_only=True) + +__all__ = ["run_extract", "resolve_models", "main"] + + +def resolve_models(cfg: DictConfig) -> List[Dict[str, Any]]: + """[{name, ckpt_path, held_out}] from extract.models or a nested-CV summary.""" + models: List[Dict[str, Any]] = [] + summary_path = cfg.extract.get("nested_cv_summary") + if summary_path: + summary = json.loads(Path(str(summary_path)).read_text()) + oof = Path(str(summary_path)).parent / "oof_predictions.csv" + held: Dict[int, List[str]] = {} + if oof.exists(): + import pandas as pd + + df = pd.read_csv(oof) + for outer, grp in df.groupby("outer"): + held[int(outer)] = sorted(grp["region_id"].astype(str).unique()) + for fold in summary["folds"]: + run_dir = Path(str(fold["run_dir"])) + ckpt = run_dir / "checkpoints" / "last.ckpt" + if not ckpt.exists(): + candidates = sorted((run_dir / "checkpoints").glob("*.ckpt")) + if not candidates: + raise FileNotFoundError(f"No checkpoint under {run_dir / 'checkpoints'}") + ckpt = candidates[-1] + models.append( + { + "name": f"fold{int(fold['outer'])}", + "ckpt_path": str(ckpt), + "held_out": held.get(int(fold["outer"]), []), + } + ) + held_cfg = dict(cfg.extract.get("held_out") or {}) + for name, path in dict(cfg.extract.get("models") or {}).items(): + held = held_cfg.get(str(name)) or [] + models.append( + {"name": str(name), "ckpt_path": str(path), "held_out": [str(r) for r in held]} + ) + if not models: + raise ValueError("Set extract.nested_cv_summary or extract.models={name: ckpt}.") + return models + + +def _regions_filter(value) -> Optional[List[str]]: + if value is None: + return None + if isinstance(value, str): + p = Path(value) + if p.suffix.lower() == ".json": + return [str(v) for v in json.loads(p.read_text())] + return [line.strip() for line in p.read_text().splitlines() if line.strip()] + return [str(v) for v in value] + + +def run_extract(cfg: DictConfig) -> Dict[str, Any]: + ecfg = cfg.extract + device = str(ecfg.get("device", "auto")).lower() + device = ( + torch.device("cuda" if torch.cuda.is_available() else "cpu") + if device == "auto" + else torch.device(device) + ) + + # All regions of the processed cohort, regardless of persisted splits. + with open_dict(cfg): + cfg.data.split.explicit = {"train": None, "val": None, "test": None, "id_kind": "region"} + datamodule = hydra.utils.instantiate(cfg.data) + datamodule.prepare_data() + datamodule.setup("fit") + full = SpatialOmicsGraphDataset(Path(str(cfg.data.processed_dir)) / "processed_index.json") + datamodule.dataset_train = full # width inference for the model build + sampler_cfg = probe_sampler_config( + datamodule.sampler_config, + roots_per_unit=None, + seed=0, + batch_size=int(ecfg.get("batch_size", 512)), + ) + ct_names = None + label_map = full.label_maps.get("cell_type") if full.label_maps else None + if label_map: + ct_names = [name for name, _ in sorted(label_map.items(), key=lambda kv: kv[1])] + out_dir = Path(str(ecfg.output_dir)) + written = [] + for spec in resolve_models(cfg): + log.info(f"[{spec['name']}] loading {spec['ckpt_path']}") + model = load_trained_module( + cfg, ckpt_path=spec["ckpt_path"], datamodule=datamodule, device=device + ) + model_dir = extract_cell_cache( + model, + full, + out_dir=out_dir, + model_name=spec["name"], + sampler_config=sampler_cfg, + ckpt_path=spec["ckpt_path"], + held_out_regions=spec.get("held_out"), + regions=_regions_filter(ecfg.get("regions")), + cell_type_names=ct_names, + autocast=bool(ecfg.get("autocast", True)), + device=device, + resume=bool(ecfg.get("resume", True)), + unit_id=str(ecfg.get("unit_id", "auto")), + log=log, + ) + written.append(str(model_dir)) + log.info(f"[{spec['name']}] cache written to {model_dir}") + del model + if device.type == "cuda": + torch.cuda.empty_cache() + return {"output_dir": str(out_dir), "models": written} + + +@hydra.main(version_base="1.3", config_path="configs", config_name="extract.yaml") +def main(cfg: DictConfig) -> None: + extras(cfg) + run_extract(cfg) + + +if __name__ == "__main__": + main() diff --git a/src/grass_mil/inference/__init__.py b/src/grass_mil/inference/__init__.py index b5db7e3..d7df6ee 100644 --- a/src/grass_mil/inference/__init__.py +++ b/src/grass_mil/inference/__init__.py @@ -1,11 +1,17 @@ from grass_mil.inference.aggregation import aggregate_group_logits +from grass_mil.inference.cell_cache import CellCache, extract_cell_cache from grass_mil.inference.collectors import collect_inference_payload -from grass_mil.inference.embeddings import embeddings_to_dataframe +from grass_mil.inference.io import embeddings_to_dataframe, node_embeddings_to_dataframe from grass_mil.inference.metrics import compute_task_metrics +from grass_mil.inference.model_loading import load_trained_module __all__ = [ + "CellCache", "aggregate_group_logits", "collect_inference_payload", "compute_task_metrics", "embeddings_to_dataframe", + "extract_cell_cache", + "load_trained_module", + "node_embeddings_to_dataframe", ] diff --git a/src/grass_mil/inference/aggregation.py b/src/grass_mil/inference/aggregation.py index 7e74ec0..a8d488d 100644 --- a/src/grass_mil/inference/aggregation.py +++ b/src/grass_mil/inference/aggregation.py @@ -10,6 +10,12 @@ def _aggregate_rows(values: torch.Tensor, mode: str) -> torch.Tensor: + if mode == "mean_probability": + # Mean of the per-row class probabilities, returned as log-probabilities + # so that softmax of the result is exactly the mean probability (patient + # score = mean of core probabilities, the preprint's default). + probs = torch.softmax(values.float(), dim=-1).mean(dim=0, keepdim=True) + return torch.log(probs.clamp_min(1e-12)) if mode == "mean": return values.mean(dim=0, keepdim=True) if mode == "max": @@ -104,7 +110,38 @@ def _normalize_group_ids_with_fallback( return resolved -def _row_group_ids(payload: BatchPredictionPayload, *, bag_scope: str) -> List[str]: +def _map_to_patient(ids: List[str], group_map: Optional[Dict[str, str]]) -> List[str]: + if group_map is None: + raise ValueError( + "bag_scope='patient' needs a region -> patient mapping (group_map); set " + "aggregation.patient_map in the predict config." + ) + out: List[str] = [] + missing: set[str] = set() + for rid in ids: + pid = group_map.get(str(rid)) + if pid is None: + missing.add(str(rid)) + pid = str(rid) + out.append(str(pid)) + if missing: + warnings.warn( + f"{len(missing)} region(s) have no patient in the mapping and were kept as their " + f"own group (first: {sorted(missing)[:5]}).", + UserWarning, + stacklevel=3, + ) + return out + + +def _row_group_ids( + payload: BatchPredictionPayload, + *, + bag_scope: str, + group_map: Optional[Dict[str, str]] = None, +) -> List[str]: + if bag_scope == "patient": + return _map_to_patient(_row_group_ids(payload, bag_scope="region"), group_map) if bag_scope == "patch": return [str(v) for v in payload.bag_ids] if bag_scope == "region": @@ -122,16 +159,26 @@ def _row_group_ids(payload: BatchPredictionPayload, *, bag_scope: str) -> List[s name="row_sample_ids", ) raise ValueError( - f"Unsupported bag_scope '{bag_scope}'. Expected one of ['patch', 'region', 'sample']." + f"Unsupported bag_scope '{bag_scope}'. Expected one of " + "['patch', 'region', 'sample', 'patient']." ) -def _instance_group_ids(payload: BatchPredictionPayload, *, bag_scope: str) -> List[str]: +def _instance_group_ids( + payload: BatchPredictionPayload, + *, + bag_scope: str, + group_map: Optional[Dict[str, str]] = None, +) -> List[str]: if payload.instance_logits is None: raise ValueError( "attention_weighted aggregation requires instance_logits in prediction payload." ) n_instances = int(payload.instance_logits.shape[0]) + if bag_scope == "patient": + # Merged bag: every instance of every core of the patient in ONE bag, so + # the attention softmax runs jointly over all of them. + return _map_to_patient(_instance_group_ids(payload, bag_scope="region"), group_map) if bag_scope == "patch": patch_ids = payload.instance_patch_ids if patch_ids is None: @@ -160,12 +207,18 @@ def _instance_group_ids(payload: BatchPredictionPayload, *, bag_scope: str) -> L name="instance_sample_ids", ) raise ValueError( - f"Unsupported bag_scope '{bag_scope}'. Expected one of ['patch', 'region', 'sample']." + f"Unsupported bag_scope '{bag_scope}'. Expected one of " + "['patch', 'region', 'sample', 'patient']." ) -def _group_row_indices(payload: BatchPredictionPayload, *, bag_scope: str) -> Dict[str, List[int]]: - row_group_ids = _row_group_ids(payload, bag_scope=bag_scope) +def _group_row_indices( + payload: BatchPredictionPayload, + *, + bag_scope: str, + group_map: Optional[Dict[str, str]] = None, +) -> Dict[str, List[int]]: + row_group_ids = _row_group_ids(payload, bag_scope=bag_scope, group_map=group_map) return _group_indices(row_group_ids) @@ -201,8 +254,16 @@ def aggregate_group_logits( bag_scope: str = "patch", subsample_fraction: float = 1.0, subsample_seed: Optional[int] = None, + group_map: Optional[Dict[str, str]] = None, ) -> AggregatedPredictionPayload: - """Aggregate duplicated bag rows into one logit row per bag.""" + """Aggregate duplicated bag rows into one logit row per bag. + + ``bag_scope='patient'`` groups regions through ``group_map`` (region id -> + patient id). With ``mode='mean_probability'`` the patient score is the mean + of its regions' class probabilities; with ``mode='attention_weighted'`` all + instances of the patient's regions form one merged bag with a joint + attention softmax. + """ _validate_subsample_fraction(subsample_fraction) if mode == "none": return AggregatedPredictionPayload( @@ -231,7 +292,7 @@ def aggregate_group_logits( "Mismatch between bag_ids and attention rows for aggregation: " f"{len(payload.bag_ids)} ids vs {len(payload.bag_attention)} attention rows." ) - grouped_row_indices = _group_row_indices(payload, bag_scope=bag_scope) + grouped_row_indices = _group_row_indices(payload, bag_scope=bag_scope, group_map=group_map) if mode == "attention_weighted": if payload.instance_attention_logits is None: @@ -239,7 +300,7 @@ def aggregate_group_logits( "attention_weighted aggregation requires instance_attention_logits " "in prediction payload." ) - instance_group_ids = _instance_group_ids(payload, bag_scope=bag_scope) + instance_group_ids = _instance_group_ids(payload, bag_scope=bag_scope, group_map=group_map) grouped_instance_indices = _group_indices(instance_group_ids) if payload.instance_attention_logits.shape[0] != payload.instance_logits.shape[0]: raise ValueError( @@ -289,8 +350,8 @@ def aggregate_group_logits( else: aggregated_row_region_ids.append(bag_id if bag_scope == "region" else None) aggregated_row_sample_ids.append(bag_id if bag_scope == "sample" else None) - elif payload.instance_logits is not None: - instance_group_ids = _instance_group_ids(payload, bag_scope=bag_scope) + elif payload.instance_logits is not None and mode != "mean_probability": + instance_group_ids = _instance_group_ids(payload, bag_scope=bag_scope, group_map=group_map) grouped_instance_indices = _group_indices(instance_group_ids) for bag_id in sorted(grouped_instance_indices): indices = grouped_instance_indices[bag_id] @@ -320,7 +381,9 @@ def aggregate_group_logits( aggregated_row_region_ids.append(bag_id if bag_scope == "region" else None) aggregated_row_sample_ids.append(bag_id if bag_scope == "sample" else None) else: - row_group_ids = _row_group_ids(payload, bag_scope=bag_scope) + # Row-level aggregation (mean_probability always works on the per-region + # rows: a patient's score is the mean of its regions' probabilities). + row_group_ids = _row_group_ids(payload, bag_scope=bag_scope, group_map=group_map) grouped_row_indices = _group_indices(row_group_ids) for bag_id in sorted(grouped_row_indices): indices = grouped_row_indices[bag_id] diff --git a/src/grass_mil/inference/cell_cache.py b/src/grass_mil/inference/cell_cache.py new file mode 100644 index 0000000..6eea7a6 --- /dev/null +++ b/src/grass_mil/inference/cell_cache.py @@ -0,0 +1,266 @@ +"""Per-cell cache of a trained GRASS-MIL model: the input of niche discovery. + +For every region and every cell (the ego-graph rooted at that cell) the cache +stores the encoder embedding ``E`` (fp16), the per-class instance logits +``LG``, the raw per-class attention logits ``AR`` and, once per region, the +head bias, cell types, positions, the cell graph and the region label. One +directory per model:: + + //index.json + //.npz + +``index.json`` records the model checkpoint, the region list with cell counts, +the head bias, the cell-type vocabulary and, when known, which regions were +held out for that model (nested CV). ``CellCache`` reads it back. +""" + +from __future__ import annotations + +import json +import time +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Dict, Iterable, List, Optional, Sequence + +import numpy as np +import torch + +__all__ = ["CellCache", "extract_cell_cache", "region_npz_path"] + +_INDEX = "index.json" + + +def region_npz_path(model_dir: Path, region_id: str) -> Path: + safe = "".join(c if c.isalnum() or c in "._-" else "_" for c in str(region_id)) + return model_dir / f"{safe}.npz" + + +@dataclass +class CellCache: + """Reader for one model's cache directory.""" + + model_dir: Path + index: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def open(cls, model_dir: str | Path) -> "CellCache": + model_dir = Path(model_dir) + index_path = model_dir / _INDEX + if not index_path.exists(): + raise FileNotFoundError(f"No cell cache index at {index_path}") + return cls(model_dir=model_dir, index=json.loads(index_path.read_text())) + + @property + def regions(self) -> List[str]: + return [r["region_id"] for r in self.index["regions"]] + + @property + def held_out(self) -> List[str]: + return list(self.index.get("held_out_regions") or []) + + @property + def bias(self) -> np.ndarray: + return np.asarray(self.index["head_bias"], dtype=np.float64) + + @property + def cell_type_names(self) -> Optional[List[str]]: + names = self.index.get("cell_type_names") + return list(names) if names else None + + def label_of(self, region_id: str) -> Optional[float]: + for r in self.index["regions"]: + if r["region_id"] == region_id: + return r.get("label") + return None + + def n_cells(self, region_id: str) -> int: + for r in self.index["regions"]: + if r["region_id"] == region_id: + return int(r["n_cells"]) + raise KeyError(region_id) + + def load(self, region_id: str, keys: Optional[Iterable[str]] = None) -> Dict[str, np.ndarray]: + path = region_npz_path(self.model_dir, region_id) + with np.load(path, allow_pickle=False) as z: + if keys is None: + return {k: z[k] for k in z.files} + return {k: z[k] for k in keys if k in z.files} + + def iter_regions(self, regions: Optional[Sequence[str]] = None, keys=None): + for rid in regions if regions is not None else self.regions: + yield rid, self.load(rid, keys) + + +def _cell_type_codes(data) -> Optional[np.ndarray]: + codes = getattr(data, "categorical_codes", None) + if codes is None: + return None + codes = codes.detach().cpu() + if codes.dim() == 2: + codes = codes[:, 0] + return codes.numpy().astype(np.int32) + + +def _roots_of(batch) -> np.ndarray: + roots = getattr(batch, "root_n_id", None) + if roots is None: + raise ValueError("ShaDow batch without root_n_id; cannot align cells.") + roots = roots.detach().cpu().long().view(-1) + n_id = getattr(batch, "n_id", None) + flag = getattr(batch, "root_n_id_is_global", None) + is_global = False + if flag is not None: + f = torch.as_tensor(flag).view(-1) + is_global = bool(f[0]) if f.numel() else False + if not is_global and n_id is not None: + roots = n_id.detach().cpu().long().view(-1)[roots] + return roots.numpy() + + +@torch.no_grad() +def extract_cell_cache( + model, + dataset, + *, + out_dir: str | Path, + model_name: str, + sampler_config, + ckpt_path: Optional[str] = None, + held_out_regions: Optional[Sequence[str]] = None, + regions: Optional[Sequence[str]] = None, + cell_type_names: Optional[Sequence[str]] = None, + autocast: bool = True, + device: Optional[torch.device | str] = None, + resume: bool = True, + unit_id: str = "auto", + log=None, +) -> Path: + """Run the model over every cell of every unit (region, sample or patch) and write the cache. + + ``unit_id='auto'`` addresses units by patch id when the dataset holds several + tiles per region and by region id otherwise. + """ + from grass_mil.data.components.samplers import get_sampler_strategy + from grass_mil.data.spatial_omics_datamodule import unit_key + + kind = str(unit_id).lower() + if kind == "auto": + regions_seen = {} + for e in dataset.entries: + regions_seen.setdefault(unit_key(e, "region"), set()).add(str(e.patch_id)) + kind = "patch" if any(len(v) > 1 for v in regions_seen.values()) else "region" + + model_dir = Path(out_dir) / model_name + model_dir.mkdir(parents=True, exist_ok=True) + device = torch.device(device) if device is not None else next(model.parameters()).device + strategy = get_sampler_strategy(sampler_config) + head_bias = _head_bias(model) + wanted = set(str(r) for r in regions) if regions is not None else None + index_regions: List[Dict[str, Any]] = [] + use_autocast = bool(autocast) and device.type == "cuda" + t0 = time.time() + for idx in range(len(dataset)): + entry = dataset.entries[idx] + rid = unit_key(entry, kind) + if wanted is not None and rid not in wanted: + continue + npz = region_npz_path(model_dir, rid) + data = dataset[idx] + n = int(data.num_nodes) + label = None + if getattr(data, "graph_y", None) is not None: + label = float(data.graph_y.view(-1)[0]) + if resume and npz.exists(): + index_regions.append({"region_id": rid, "n_cells": n, "label": label}) + continue + E = np.zeros((n, int(model.encoder.output_dim)), dtype=np.float16) + LG = None + AR = None + seen = np.zeros(n, dtype=bool) + loader = strategy.build_dataset_loader( + dataset=[data], batch_size=1, num_workers=0, pin_memory=False, shuffle=False + ) + for batch in loader: + roots = _roots_of(batch) + batch = batch.to(device) + with torch.autocast(device.type, dtype=torch.float16, enabled=use_autocast): + logits, emb, _ = model._forward_instances(batch) + attn = model.attention(emb)[0] if model.attention is not None else None + emb = emb.float() + if LG is None: + LG = np.zeros((n, int(logits.shape[1])), dtype=np.float32) + AR = ( + np.zeros((n, int(attn.shape[1])), dtype=np.float32) + if attn is not None + else None + ) + E[roots] = emb.half().cpu().numpy() + LG[roots] = logits.float().cpu().numpy() + if AR is not None: + AR[roots] = attn.float().cpu().numpy() + seen[roots] = True + if not seen.all(): + raise RuntimeError( + f"Region {rid}: {int((~seen).sum())} of {n} cells were not visited; " + "the extraction sampler must keep every root (all-roots mode)." + ) + payload: Dict[str, Any] = { + "E": E, + "LG": LG, + "pos": data.pos.detach().cpu().numpy().astype(np.float32), + "edge_index": data.edge_index.detach().cpu().numpy().astype(np.int64), + } + if AR is not None: + payload["AR"] = AR + ct = _cell_type_codes(data) + if ct is not None: + payload["ct"] = ct + if getattr(data, "edge_attr", None) is not None: + payload["edge_attr"] = data.edge_attr.detach().cpu().numpy().astype(np.float32) + np.savez(npz, **payload) + index_regions.append({"region_id": rid, "n_cells": n, "label": label}) + if log is not None and len(index_regions) % 25 == 0: + log.info( + f"[{model_name}] {len(index_regions)} regions ({(time.time() - t0) / 60:.1f} min)" + ) + index = { + "model_name": model_name, + "ckpt_path": str(ckpt_path) if ckpt_path else None, + "regions": index_regions, + "held_out_regions": [str(r) for r in held_out_regions] if held_out_regions else [], + "head_bias": head_bias, + "unit_id": kind, + "cell_type_names": list(cell_type_names) if cell_type_names else None, + "embedding_dim": int(model.encoder.output_dim), + "sampler_runtime": _plain(getattr(sampler_config, "runtime", {}) or {}), + } + (model_dir / _INDEX).write_text(json.dumps(index, indent=1, default=str)) + return model_dir + + +def _plain(obj: Any) -> Any: + try: + from omegaconf import DictConfig, ListConfig, OmegaConf + + if isinstance(obj, (DictConfig, ListConfig)): + return OmegaConf.to_container(obj, resolve=True) + except ImportError: # pragma: no cover + pass + if isinstance(obj, dict): + return {str(k): _plain(v) for k, v in obj.items()} + if isinstance(obj, (list, tuple)): + return [_plain(v) for v in obj] + return obj + + +def _head_bias(model) -> List[float]: + head = getattr(model, "graph_head", None) + if head is None: + return [] + last = None + for module in head.modules(): + if isinstance(module, torch.nn.Linear): + last = module + if last is None or last.bias is None: + return [] + return [float(v) for v in last.bias.detach().cpu().view(-1)] diff --git a/src/grass_mil/inference/embeddings.py b/src/grass_mil/inference/embeddings.py deleted file mode 100644 index b007159..0000000 --- a/src/grass_mil/inference/embeddings.py +++ /dev/null @@ -1,18 +0,0 @@ -from __future__ import annotations - -import pandas as pd - -from grass_mil.inference.io import ( - embeddings_to_dataframe as _embeddings_to_dataframe, - node_embeddings_to_dataframe as _node_embeddings_to_dataframe, -) -from grass_mil.inference.schemas import EmbeddingPayload - - -def embeddings_to_dataframe(payload: EmbeddingPayload) -> pd.DataFrame: - """Backwards-friendly alias for table conversion.""" - return _embeddings_to_dataframe(payload) - - -def node_embeddings_to_dataframe(payload: EmbeddingPayload) -> pd.DataFrame: - return _node_embeddings_to_dataframe(payload) diff --git a/src/grass_mil/inference/interpretability_export.py b/src/grass_mil/inference/interpretability_export.py index ce05cfe..dc36876 100644 --- a/src/grass_mil/inference/interpretability_export.py +++ b/src/grass_mil/inference/interpretability_export.py @@ -15,6 +15,8 @@ def _tensor_to_2d_cpu(values: Optional[torch.Tensor]) -> Optional[torch.Tensor]: if values is None: return None values = values.detach().cpu() + if values.is_floating_point(): + values = values.float() if values.ndim == 1: values = values.unsqueeze(-1) return values diff --git a/src/grass_mil/inference/io.py b/src/grass_mil/inference/io.py index 7c73dde..dd9983e 100644 --- a/src/grass_mil/inference/io.py +++ b/src/grass_mil/inference/io.py @@ -19,6 +19,8 @@ def ensure_output_dir(base_output_dir: Path, output_subdir: str) -> Path: def _tensor_columns(prefix: str, values: torch.Tensor) -> Dict[str, Any]: values = values.detach().cpu() + if values.is_floating_point(): + values = values.float() # bf16/fp16 autocast outputs have no numpy dtype if values.ndim == 1: values = values.unsqueeze(-1) data: Dict[str, Any] = {} diff --git a/src/grass_mil/inference/model_loading.py b/src/grass_mil/inference/model_loading.py new file mode 100644 index 0000000..046745b --- /dev/null +++ b/src/grass_mil/inference/model_loading.py @@ -0,0 +1,69 @@ +"""Build a supervised module from config and load trained weights outside ``Trainer``.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any, Dict, Optional + +import hydra +import torch +from omegaconf import DictConfig + +from grass_mil.models.training.probe import attach_datamodule + +__all__ = ["load_trained_module", "read_checkpoint_state"] + + +def read_checkpoint_state(ckpt_path: str | Path) -> Dict[str, torch.Tensor]: + raw = torch.load(str(ckpt_path), map_location="cpu", weights_only=False) + state = raw["state_dict"] if isinstance(raw, dict) and "state_dict" in raw else raw + if not isinstance(state, dict): + raise ValueError(f"{ckpt_path} does not hold a state dict.") + return state + + +def load_trained_module( + cfg: DictConfig, + *, + ckpt_path: str | Path, + datamodule: Any, + device: Optional[torch.device | str] = None, + strict: bool = True, +): + """Instantiate ``cfg.model``, bind ``datamodule`` and load ``ckpt_path``. + + The datamodule must already be set up (``setup("fit")``) so the module can + infer its input widths. Returns the module in eval mode on ``device``. + """ + import copy + + from omegaconf import open_dict + + cfg = copy.deepcopy(cfg) + with open_dict(cfg): + # The checkpoint carries the trained head; a probe pre-fit request + # (linear_* regimes: task.head_init_path=auto) is a training-time step. + if "task" in cfg and cfg.task.get("head_init_path") is not None: + cfg.task.head_init_path = None + if "init_from_ckpt" in cfg.model: + cfg.model.init_from_ckpt = None + model = hydra.utils.instantiate(cfg.model) + trainer = hydra.utils.instantiate(cfg.trainer, logger=False, enable_checkpointing=False) + attach_datamodule(model, datamodule, trainer) + model.setup("fit") + state = read_checkpoint_state(ckpt_path) + missing, unexpected = model.load_state_dict(state, strict=False) + if strict: + missing = [k for k in missing if not k.startswith("loss_fn.")] + if missing or unexpected: + raise ValueError( + f"Checkpoint {ckpt_path} does not match the model: missing {missing[:5]}, " + f"unexpected {unexpected[:5]}. Compose the same model/regime as the run." + ) + if device is None: + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + model.to(torch.device(device)) + model.eval() + for p in model.parameters(): + p.requires_grad_(False) + return model diff --git a/src/grass_mil/inference/predict.py b/src/grass_mil/inference/predict.py index e97b73c..a2aa769 100644 --- a/src/grass_mil/inference/predict.py +++ b/src/grass_mil/inference/predict.py @@ -1,7 +1,7 @@ from __future__ import annotations from pathlib import Path -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List, Optional, Tuple import hydra from lightning import LightningDataModule, LightningModule, Trainer @@ -192,6 +192,24 @@ def _preforward_decision_to_dict( } +def _load_patient_map(cfg_map) -> Optional[Dict[str, str]]: + """Region -> patient mapping from ``aggregation.patient_map`` (label CSV columns).""" + if not cfg_map or not cfg_map.get("label_file"): + return None + import pandas as pd + + df = pd.read_csv(str(cfg_map.label_file)) + df.columns = [str(c).strip() for c in df.columns] + id_col = str(cfg_map.get("id_column", "REGION_ID")) + pat_col = str(cfg_map.get("patient_column", "PATIENT")) + for col in (id_col, pat_col): + if col not in df.columns: + raise ValueError( + f"aggregation.patient_map: column '{col}' not in {cfg_map.label_file}" + ) + return {str(r): str(p) for r, p in zip(df[id_col], df[pat_col])} + + @task_wrapper def predict(cfg: DictConfig) -> Tuple[Dict[str, Any], Dict[str, Any]]: ckpt_path = cfg.get("ckpt_path") @@ -348,6 +366,7 @@ def predict(cfg: DictConfig) -> Tuple[Dict[str, Any], Dict[str, Any]]: bag_scope=str(cfg.aggregation.get("bag_scope", "patch")), subsample_fraction=aggregation_subsample_fraction, subsample_seed=aggregation_subsample_seed, + group_map=_load_patient_map(cfg.aggregation.get("patient_map")), ) aggregation_metadata = dict(getattr(aggregated, "metadata", {})) aggregation_metadata["preforward_subsampling_applied"] = bool( diff --git a/src/grass_mil/interpretability/__init__.py b/src/grass_mil/interpretability/__init__.py index 9a59eef..1f3d041 100644 --- a/src/grass_mil/interpretability/__init__.py +++ b/src/grass_mil/interpretability/__init__.py @@ -1,7 +1,14 @@ from grass_mil.interpretability.core.biomarkers import ( niche_composition_summary, ) -from grass_mil.interpretability.core.clustering import run_clustering +from grass_mil.interpretability.core.clustering import ( + cosine_hdbscan, + fit_niche_clustering, + run_clustering, +) +from grass_mil.interpretability.core.attribution import niche_attribution_summary +from grass_mil.interpretability.core.agreement import compute_niche_agreement +from grass_mil.interpretability.niches import run_niche_discovery from grass_mil.interpretability.core.reduction import run_reduction from grass_mil.interpretability.core.transfer import ( apply_niche_transfer, @@ -24,6 +31,11 @@ __all__ = [ "run_reduction", "run_clustering", + "cosine_hdbscan", + "fit_niche_clustering", + "run_niche_discovery", + "niche_attribution_summary", + "compute_niche_agreement", "niche_composition_summary", "run_neighborhood_enrichment", "run_diff_neighborhood_enrichment", diff --git a/src/grass_mil/interpretability/core/clustering.py b/src/grass_mil/interpretability/core/clustering.py index ac10e78..5bd9c47 100644 --- a/src/grass_mil/interpretability/core/clustering.py +++ b/src/grass_mil/interpretability/core/clustering.py @@ -70,3 +70,240 @@ def run_clustering( params=all_params, fitted_object=model, ) + + +# ---------------------------------------------------------------------------- +# Sample -> reduce -> (L2) -> cluster -> assign: the GRASS-MIL niche recipe +# ---------------------------------------------------------------------------- + + +_HDBSCAN_ONLY = ( + "min_cluster_size", + "min_samples", + "cluster_selection_method", + "cluster_selection_epsilon", + "allow_single_cluster", + "alpha", + "max_cluster_size", + "store_centers", +) + + +class NicheClusterModel: + """A fitted "reduce on a sample, cluster, assign the rest" model. + + ``predict(X)`` reduces new embeddings with the fitted reducer and assigns + them with the k-NN classifier fitted on the clustered sample (HDBSCAN has + no native predict; k-NN on the reduced coordinates is what the preprint + uses). Backends with their own ``predict`` (k-means) use it directly. + """ + + def __init__( + self, + *, + reducer, + clusterer, + assigner, + sample_idx: np.ndarray, + labels_sample: np.ndarray, + params: Dict[str, Any], + l2_normalise: bool, + reducer_method: str, + clustering_method: str, + ) -> None: + self.reducer = reducer + self.clusterer = clusterer + self.assigner = assigner + self.sample_idx = np.asarray(sample_idx) + self.labels_sample = np.asarray(labels_sample) + self.params = dict(params) + self.l2_normalise = bool(l2_normalise) + self.reducer_method = reducer_method + self.clustering_method = clustering_method + + def reduce(self, X: np.ndarray) -> np.ndarray: + X = np.asarray(X, dtype=np.float32) + return X if self.reducer is None else self.reducer.transform(X) + + def predict(self, X: np.ndarray) -> np.ndarray: + P = self.reduce(X) + if self.assigner is not None: + return np.asarray(self.assigner.predict(P)) + from sklearn.preprocessing import normalize + + Pc = normalize(P, norm="l2") if self.l2_normalise else P + return np.asarray(self.clusterer.predict(Pc)) + + @property + def n_clusters(self) -> int: + return int(len(set(self.labels_sample.tolist()) - {-1})) + + +def fit_niche_clustering( + X: np.ndarray, + *, + reduction: Optional[Dict[str, Any]] = None, + clustering: Optional[Dict[str, Any]] = None, + sample_size: Optional[int] = 100_000, + l2_normalise: bool = True, + assign_k: int = 15, + seed: int = 0, +) -> tuple[NicheClusterModel, np.ndarray]: + """Fit the niche clustering and label every row of ``X``. + + Default (preprint): PCA to 10 components fitted on a random sample of at + most ``sample_size`` rows, L2 normalisation (so euclidean HDBSCAN acts on + the cosine geometry), ``HDBSCAN(min_cluster_size=300, min_samples=1, + cluster_selection_method="eom")`` on the sample, and 15-nearest-neighbour + assignment of the remaining rows on the (un-normalised) PCs. + + ``reduction`` = ``{"method": "pca"|"none", "n_components": int, ...}``; + ``clustering`` = ``{"method": , "params": {...}, + "n_clusters": int}``. Any backend of :func:`run_clustering` is accepted. + Returns ``(model, labels_all)`` with ``-1`` for noise. + """ + from sklearn.neighbors import KNeighborsClassifier + from sklearn.preprocessing import normalize + + from grass_mil.interpretability.core.reduction import run_reduction + + X = np.asarray(X, dtype=np.float32) + n = int(X.shape[0]) + reduction = dict(reduction or {"method": "pca", "n_components": 10}) + clustering = dict( + clustering + or { + "method": "hdbscan", + "params": { + "min_cluster_size": 300, + "min_samples": 1, + "cluster_selection_method": "eom", + }, + } + ) + rng = np.random.default_rng(int(seed)) + if sample_size is None or int(sample_size) >= n: + sample_idx = np.arange(n) + else: + sample_idx = np.sort(rng.choice(n, int(sample_size), replace=False)) + + red_method = str(reduction.get("method", "pca")).strip().lower() + reducer = None + if red_method not in {"none", "identity"}: + red_params = { + k: v + for k, v in reduction.items() + if k not in {"method", "n_components", "random_state"} + } + n_components = int(reduction.get("n_components", 10)) + n_components = max(1, min(n_components, int(X.shape[1]), int(sample_idx.size))) + res = run_reduction( + red_method, + X[sample_idx], + n_components=n_components, + random_state=int(reduction.get("random_state", 42)), + **red_params, + ) + reducer = res.fitted_object + P_sample = X[sample_idx] if reducer is None else reducer.transform(X[sample_idx]) + P_cluster = normalize(P_sample, norm="l2") if l2_normalise else P_sample + + cl_method = str(clustering.get("method", "hdbscan")).strip().lower() + cl_params = dict(clustering.get("params", {}) or {}) + if cl_method == "hdbscan" and l2_normalise: + cl_params.pop("metric", None) + if cl_method != "hdbscan": + # The default params block is HDBSCAN's; switching the backend on the + # command line must not forward keys the other estimator does not take. + for key in _HDBSCAN_ONLY: + cl_params.pop(key, None) + if cl_method == "hdbscan": + cl_params.setdefault("min_cluster_size", 300) + cl_params.setdefault("min_samples", 1) + cl_params.setdefault("cluster_selection_method", "eom") + cl_params["min_cluster_size"] = int( + min(int(cl_params["min_cluster_size"]), max(2, sample_idx.size)) + ) + result = run_clustering( + cl_method, + P_cluster, + n_clusters=clustering.get("n_clusters"), + random_state=int(clustering.get("random_state", 42)), + **cl_params, + ) + labels_sample = np.asarray(result.labels).astype(int) + + # Backends with a native predict (k-means, ...) assign unsampled rows + # themselves, exactly reproducing the sample labels; density backends + # (HDBSCAN, DBSCAN, OPTICS) have none, so a k-NN classifier on the reduced + # sample coordinates assigns the rest (the preprint's 15-NN step). + assigner = None + if not hasattr(result.fitted_object, "predict"): + k = max(1, min(int(assign_k) if int(assign_k) > 0 else 15, int(sample_idx.size))) + assigner = KNeighborsClassifier(k).fit(P_sample, labels_sample) + model = NicheClusterModel( + reducer=reducer, + clusterer=result.fitted_object, + assigner=assigner, + sample_idx=sample_idx, + labels_sample=labels_sample, + params={ + "reduction": { + "method": red_method, + **( + {"n_components": int(reducer.n_components_)} + if reducer is not None and hasattr(reducer, "n_components_") + else {} + ), + }, + "clustering": { + "method": cl_method, + **cl_params, + **( + {"n_clusters": clustering.get("n_clusters")} + if clustering.get("n_clusters") + else {} + ), + }, + "sample_size": int(sample_idx.size), + "l2_normalise": bool(l2_normalise), + "assign_k": int(assign_k), + "seed": int(seed), + }, + l2_normalise=l2_normalise, + reducer_method=red_method, + clustering_method=cl_method, + ) + labels_all = model.predict(X) + labels_all[sample_idx] = labels_sample + return model, labels_all + + +def cosine_hdbscan( + X: np.ndarray, + *, + n_components: int = 10, + min_cluster_size: int = 300, + min_samples: Optional[int] = 1, + cluster_selection_method: str = "eom", + sample_size: Optional[int] = 100_000, + assign_k: int = 15, + seed: int = 0, +) -> tuple[NicheClusterModel, np.ndarray]: + """The preprint's niche clustering with its defaults (see :func:`fit_niche_clustering`).""" + return fit_niche_clustering( + X, + reduction={"method": "pca", "n_components": int(n_components)}, + clustering={ + "method": "hdbscan", + "params": { + "min_cluster_size": int(min_cluster_size), + "min_samples": min_samples, + "cluster_selection_method": str(cluster_selection_method), + }, + }, + sample_size=sample_size, + l2_normalise=True, + assign_k=assign_k, + seed=seed, + ) diff --git a/src/grass_mil/interpretability/metaclusters/__init__.py b/src/grass_mil/interpretability/metaclusters/__init__.py new file mode 100644 index 0000000..a23c872 --- /dev/null +++ b/src/grass_mil/interpretability/metaclusters/__init__.py @@ -0,0 +1,22 @@ +"""Cross-fold niche replication: per-fold clustering, matching into metaclusters, +whole-dataset transfer and a shared label-free basis.""" + +from .basis import run_shared_basis +from .match import ( + constrained_average_linkage, + hellinger_matrix, + match_metaclusters, + scan_threshold, +) +from .records import cluster_fold_models +from .transfer import run_transfer + +__all__ = [ + "cluster_fold_models", + "constrained_average_linkage", + "hellinger_matrix", + "match_metaclusters", + "run_shared_basis", + "run_transfer", + "scan_threshold", +] diff --git a/src/grass_mil/interpretability/metaclusters/basis.py b/src/grass_mil/interpretability/metaclusters/basis.py new file mode 100644 index 0000000..11a7ca4 --- /dev/null +++ b/src/grass_mil/interpretability/metaclusters/basis.py @@ -0,0 +1,252 @@ +"""Stage 4: a shared, label-free k-means basis with per-model attribution. + +Fit ``MiniBatchKMeans(k)`` once on standardised embeddings of ONE space (a +chosen model, or a pretrained encoder cache), label every cell, then report +per niche the relattn margin of every fold model split into held-out / train / +all (model, region) pairs with bootstrap intervals, next to the empirical +association. Comparable across models because the niches are the same. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, Optional, Sequence + +import numpy as np +import pandas as pd + +from grass_mil.inference.cell_cache import CellCache +from grass_mil.interpretability.niches.margins import relattn_margins +from grass_mil.interpretability.niches.pipeline import load_cells +from grass_mil.interpretability.niches.tables import bootstrap_mean + +__all__ = ["BasisResult", "run_shared_basis", "write_basis_outputs"] + + +@dataclass +class BasisResult: + labels: np.ndarray # (N_all,) niche id + region_ids: np.ndarray + pairs: pd.DataFrame # (model, region, niche, set, relattn, lift, n) + table: pd.DataFrame + basis_model: Any + scaler: Any + settings: Dict[str, Any] + + +def run_shared_basis( + caches: Dict[str, CellCache], + *, + basis_cache: Optional[CellCache] = None, + labels_of: Dict[str, int], + k: int = 20, + sample_size: int = 300_000, + seed: int = 0, + n_init: int = 10, + batch_size: int = 4096, + regions: Optional[Sequence[str]] = None, + positive_class: int = 1, + n_boot: int = 2000, + log=None, +) -> BasisResult: + from sklearn.cluster import MiniBatchKMeans + from sklearn.preprocessing import StandardScaler + + models = list(caches) + basis = basis_cache if basis_cache is not None else caches[models[0]] + regions = sorted(str(r) for r in (regions if regions is not None else basis.regions)) + cells_b = load_cells(basis, regions) + RID = cells_b.region_ids + E = cells_b.E + rng = np.random.default_rng(int(seed)) + sub = rng.choice(len(E), min(int(sample_size), len(E)), replace=False) + scaler = StandardScaler().fit(E[sub]) + km = MiniBatchKMeans( + int(k), random_state=int(seed), n_init=int(n_init), batch_size=int(batch_size) + ).fit(scaler.transform(E[sub])) + labels = km.predict(scaler.transform(E)).astype(int) + fold_of_region = {str(r): name for name, c in caches.items() for r in c.held_out} + rows = [] + for name, cache in caches.items(): + cells = load_cells(cache, regions) + if not np.array_equal(cells.region_ids, RID): + raise ValueError(f"Cache {name} does not cover the same cells as the basis cache.") + rel = relattn_margins(cells.LG, cells.AR, cells.bias, RID, positive_class=positive_class) + a_pos = rel.attention[:, min(positive_class, rel.attention.shape[1] - 1)] + for r in regions: + m = RID == r + lab = labels[m] + mm = rel.margin[m] + aa = a_pos[m] + which = "heldout" if fold_of_region.get(r) == name else "train" + for cl in np.unique(lab): + msk = lab == cl + rows.append( + dict( + model=name, + region=r, + niche=int(cl), + set=which, + relattn=float(mm[msk].mean()), + lift=float(aa[msk].sum() / msk.mean()), + n=int(msk.sum()), + ) + ) + if log is not None: + log.info(f"[{name}] basis attribution over {len(regions)} regions") + pairs = pd.DataFrame(rows) + names = cells_b.cell_type_names + K = len(names) + ct = cells_b.ct + y_cell = np.array([int(labels_of[r]) for r in RID]) + cohort = np.bincount(ct, minlength=K) / max(1, ct.size) + out = [] + for cl in sorted(np.unique(labels)): + msk = labels == cl + comp = np.bincount(ct[msk], minlength=K) / max(1, msk.sum()) + enr = comp / np.maximum(cohort, 1e-9) + rec = dict( + niche=f"N{int(cl)}", + niche_id=int(cl), + share=float(msk.mean()), + regions=int(pairs[pairs.niche == cl].region.nunique()), + emp_frac_pos=float(y_cell[msk].mean()), + top_types=", ".join(f"{names[j]} {100 * comp[j]:.0f}%" for j in np.argsort(-comp)[:3]), + top_enriched=", ".join( + f"{names[j]} x{enr[j]:.1f}" for j in np.argsort(-enr)[:3] if enr[j] > 1.5 + ), + ) + for s in ("heldout", "train"): + v = pairs[(pairs.niche == cl) & (pairs.set == s)] + rec[f"relattn_{s}"], rec[f"relattn_{s}_lo"], rec[f"relattn_{s}_hi"] = bootstrap_mean( + v.relattn, n_boot=n_boot, seed=seed + ) + rec[f"lift_{s}"] = float(v.lift.mean()) if len(v) else float("nan") + v = pairs[pairs.niche == cl] + rec["relattn_all"], rec["relattn_all_lo"], rec["relattn_all_hi"] = bootstrap_mean( + v.relattn, n_boot=n_boot, seed=seed + ) + rec["lift_all"] = float(v.lift.mean()) if len(v) else float("nan") + rec.update({f"comp_{names[j]}": float(comp[j]) for j in range(K)}) + out.append(rec) + table = pd.DataFrame(out) + return BasisResult( + labels=labels, + region_ids=RID, + pairs=pairs, + table=table, + basis_model=km, + scaler=scaler, + settings={ + "k": int(k), + "sample_size": int(sub.size), + "seed": int(seed), + "basis": basis.index.get("model_name"), + "models": models, + "n_boot": int(n_boot), + }, + ) + + +def write_basis_outputs( + result: BasisResult, + out_dir: str | Path, + *, + positive_name: str = "positive", + figures: bool = True, +) -> Dict[str, str]: + import joblib + + out = Path(out_dir) + out.mkdir(parents=True, exist_ok=True) + np.save(out / "basis_labels.npy", result.labels) + joblib.dump( + {"kmeans": result.basis_model, "scaler": result.scaler, "settings": result.settings}, + out / "basis_model.pkl", + ) + result.pairs.to_csv(out / "basis_attribution_pairs.csv", index=False) + result.table.to_csv(out / "basis_attribution.csv", index=False) + T = result.table + md = [ + f"# Shared k-means basis (k = {result.settings['k']}, fitted on {result.settings['basis']}) with per-model attribution", + "", + f"Margins = relattn, region-equal mean, 95% bootstrap over (model, region) pairs; held-out = region scored by the model that held it out; {len(result.settings['models'])} models.", + "", + "| Niche | Share | Regions | Top types | Top enriched | Emp. frac. pos | Margin HELD-OUT | Margin TRAIN | Margin ALL | Lift held-out / train |", + "|---|---|---|---|---|---|---|---|---|---|", + ] + for _, t in T.sort_values("emp_frac_pos", ascending=False).iterrows(): + md.append( + f"| {t.niche} | {100 * t.share:.1f}% | {t.regions} | {t.top_types} | {t.top_enriched} | {t.emp_frac_pos:.2f} | {t.relattn_heldout:+.2f} [{t.relattn_heldout_lo:+.2f}, {t.relattn_heldout_hi:+.2f}] | {t.relattn_train:+.2f} [{t.relattn_train_lo:+.2f}, {t.relattn_train_hi:+.2f}] | {t.relattn_all:+.2f} [{t.relattn_all_lo:+.2f}, {t.relattn_all_hi:+.2f}] | {t.lift_heldout:.2f} / {t.lift_train:.2f} |" + ) + if len(T) >= 3: + from scipy.stats import spearmanr + + for s in ("heldout", "train", "all"): + md.append( + f"\nSpearman(emp_frac_pos, margin_{s}) over {len(T)} niches: rho={spearmanr(T.emp_frac_pos, T[f'relattn_{s}'], nan_policy='omit')[0]:+.2f}" + ) + (out / "report_basis.md").write_text("\n".join(md) + "\n") + paths = {"report": str(out / "report_basis.md")} + if figures: + try: + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + T2 = T.sort_values("emp_frac_pos", ascending=False).reset_index(drop=True) + base = float(np.average(T2.emp_frac_pos, weights=T2.share)) + yy = np.arange(len(T2))[::-1] + fig, ax = plt.subplots( + 1, 2, figsize=(10, 0.45 * len(T2) + 1.8), gridspec_kw=dict(width_ratios=[1, 1.6]) + ) + ax[0].barh( + yy, + T2.emp_frac_pos, + color=["#b2182b" if v > base else "#2166ac" for v in T2.emp_frac_pos], + height=0.6, + ) + ax[0].axvline(base, color="k", ls=":", lw=1) + ax[0].set_yticks(yy) + ax[0].set_yticklabels( + [ + f"{n} ({100 * s:.1f}%): {t.split(',')[0]}" + for n, s, t in zip(T2.niche, T2.share, T2.top_enriched) + ], + fontsize=7, + ) + ax[0].set_xlabel(f"fraction of niche cells from {positive_name} regions") + for s, col, off in ( + ("heldout", "#1b9e77", 0.22), + ("train", "#d95f02", 0.0), + ("all", "#7570b3", -0.22), + ): + m_, lo, hi = T2[f"relattn_{s}"], T2[f"relattn_{s}_lo"], T2[f"relattn_{s}_hi"] + ax[1].errorbar( + m_, + yy + off, + xerr=[m_ - lo, hi - m_], + fmt="o", + ms=4, + color=col, + capsize=2, + lw=1, + label=s, + ) + ax[1].axvline(0, color="k", lw=0.8) + ax[1].set_yticks(yy) + ax[1].set_yticklabels([]) + ax[1].set_xlabel(f"attention-weighted margin (relattn), + = {positive_name}") + ax[1].legend(fontsize=7, loc="lower right") + for a in ax: + a.spines["top"].set_visible(False) + a.spines["right"].set_visible(False) + fig.tight_layout() + fig.savefig(out / "basis_attribution.png", dpi=160) + plt.close(fig) + paths["figure"] = str(out / "basis_attribution.png") + except Exception: # pragma: no cover + pass + return paths diff --git a/src/grass_mil/interpretability/metaclusters/match.py b/src/grass_mil/interpretability/metaclusters/match.py new file mode 100644 index 0000000..4c995ff --- /dev/null +++ b/src/grass_mil/interpretability/metaclusters/match.py @@ -0,0 +1,420 @@ +"""Stage 2: match fold-clusters across folds into metaclusters. + +Distance between two fold-clusters = Hellinger distance of their cell-type +compositions (or Aitchison / CLR euclidean). Constrained average linkage never +merges two clusters of the same fold (cannot-link). The threshold is either +fixed or chosen by a scan that maximises the number of metaclusters replicated +in at least ``min_folds`` folds (ties -> fewer metaclusters). +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Dict, List, Optional, Sequence, Tuple + +import numpy as np +import pandas as pd + +__all__ = [ + "MatchResult", + "aitchison_matrix", + "constrained_average_linkage", + "hellinger_matrix", + "match_metaclusters", + "metacluster_table", + "patient_shares", + "patient_tests", + "scan_threshold", +] + + +def hellinger_matrix(comp: np.ndarray) -> np.ndarray: + """Pairwise Hellinger distance between composition rows (in [0, 1]).""" + H = np.sqrt(np.clip(np.asarray(comp, dtype=float), 0.0, None)) + return np.sqrt(0.5 * ((H[:, None, :] - H[None, :, :]) ** 2).sum(-1)) + + +def aitchison_matrix(comp: np.ndarray, pseudo: float = 1e-3) -> np.ndarray: + L = np.log(np.asarray(comp, dtype=float) + pseudo) + L = L - L.mean(axis=1, keepdims=True) + return np.sqrt(((L[:, None, :] - L[None, :, :]) ** 2).sum(-1)) + + +def constrained_average_linkage( + D: np.ndarray, fold: Sequence[Any], threshold: float +) -> List[List[int]]: + """Agglomerate greedily by average distance below ``threshold``; groups sharing a fold never merge.""" + fold = np.asarray(fold) + n = D.shape[0] + groups: List[List[int]] = [[i] for i in range(n)] + while True: + best = None + for a in range(len(groups)): + fa = set(fold[groups[a]].tolist()) + for b in range(a + 1, len(groups)): + if fa & set(fold[groups[b]].tolist()): + continue + d = float(np.mean([D[i, j] for i in groups[a] for j in groups[b]])) + if d < threshold and (best is None or d < best[0]): + best = (d, a, b) + if best is None: + break + _, a, b = best + groups[a] = groups[a] + groups[b] + del groups[b] + return groups + + +def scan_threshold( + D: np.ndarray, fold: Sequence[Any], *, grid: Sequence[float], min_folds: int = 3 +) -> Tuple[float, List[List[int]], pd.DataFrame]: + """Pick the threshold maximising replicated metaclusters, ties -> fewer metaclusters.""" + rows = [] + best = None + for thr in grid: + groups = constrained_average_linkage(D, fold, float(thr)) + rep = sum(len(g) >= int(min_folds) for g in groups) + key = (rep, -len(groups)) + rows.append({"threshold": float(thr), "n_metaclusters": len(groups), "n_replicated": rep}) + if best is None or key > best[0]: + best = (key, float(thr), groups) + return best[1], best[2], pd.DataFrame(rows) + + +@dataclass +class MatchResult: + threshold: float + distance: str + records: pd.DataFrame # fold-cluster records with a `meta` column + table: pd.DataFrame # one row per metacluster + D: np.ndarray + scan: Optional[pd.DataFrame] = None + patient_shares: Optional[pd.DataFrame] = None + patient_tests: Optional[pd.DataFrame] = None + region_shares: Optional[pd.DataFrame] = None + settings: Dict[str, Any] = field(default_factory=dict) + + +def metacluster_table( + records: pd.DataFrame, D: np.ndarray, cohort: np.ndarray, names: Sequence[str] +) -> pd.DataFrame: + comp_cols = [f"comp_{n}" for n in names] + rows = [] + for k in sorted(records["meta"].unique()): + g = records[records["meta"] == k] + comp = np.average( + g[comp_cols].to_numpy(dtype=float), axis=0, weights=g["n_cells"].to_numpy(dtype=float) + ) + enr = comp / np.maximum(np.asarray(cohort, dtype=float), 1e-9) + idx = g.index.to_list() + within = ( + float(np.mean([D[i, j] for i in idx for j in idx if i < j])) if len(idx) > 1 else 0.0 + ) + rows.append( + dict( + meta=int(k), + n_folds=int(g["fold"].nunique()), + n_members=int(len(g)), + folds=",".join(str(f) for f in sorted(g["fold"].unique())), + members=", ".join(g["id"]), + within_dist=within, + share_mean=float(g["share"].mean()), + cores=int(g["cores"].sum()), + pos_cores=int(g["pos_cores"].sum()), + neg_cores=int(g["neg_cores"].sum()), + emp_mean=float(g["emp"].mean()), + emp_rel_mean=float(g["emp_rel"].mean()), + emp_rel_sd=float(g["emp_rel"].std(ddof=1)) if len(g) > 1 else float("nan"), + emp_per_fold=";".join(f"{f}:{e:+.2f}" for f, e in zip(g["fold"], g["emp_rel"])), + margin_mean=float(g["margin"].mean()), + margin_sd=float(g["margin"].std(ddof=1)) if len(g) > 1 else float("nan"), + margin_per_fold=";".join(f"{f}:{m:+.2f}" for f, m in zip(g["fold"], g["margin"])), + sign_agree=float(np.mean(np.sign(g["margin"]) == np.sign(g["margin"].mean()))), + ci_excl0=int(((g["margin_lo"] > 0) | (g["margin_hi"] < 0)).sum()), + emp_margin_agree=float(np.mean(np.sign(g["emp_rel"]) == np.sign(g["margin"]))), + lift_mean=float(g["lift"].mean()), + top_types=", ".join( + f"{names[j]} {100 * comp[j]:.0f}%" for j in np.argsort(-comp)[:4] + ), + top_enriched=", ".join( + f"{names[j]} x{enr[j]:.1f}" for j in np.argsort(-enr)[:4] if enr[j] > 1.5 + ), + **{c: float(v) for c, v in zip(comp_cols, comp)}, + ) + ) + return ( + pd.DataFrame(rows) + .sort_values(["n_folds", "emp_rel_mean"], ascending=[False, False]) + .reset_index(drop=True) + ) + + +def patient_shares( + region_labels: Dict[str, np.ndarray], + region_fold: Dict[str, str], + id_to_meta: Dict[str, int], + patient_of: Dict[str, str], + metas: Sequence[int], +) -> Tuple[pd.DataFrame, pd.DataFrame]: + """Per-region and per-patient metacluster shares (each region labelled once).""" + cols = [f"M{k}" for k in metas] + regions = sorted(region_labels) + SH = pd.DataFrame(0.0, index=regions, columns=cols) + for r in regions: + lab = region_labels[r] + f = region_fold[r] + for c in set(lab.tolist()) - {-1}: + k = id_to_meta.get(f"{f}_C{c}") + if k is not None: + SH.loc[r, f"M{k}"] += float(np.mean(lab == c)) + SH.index.name = "region_id" + pat = SH.copy() + pat["patient_id"] = [patient_of.get(r, r) for r in regions] + return SH, pat.groupby("patient_id").mean() + + +def patient_tests( + shares: pd.DataFrame, y: pd.Series, *, present_threshold: float = 0.0 +) -> pd.DataFrame: + """Mann-Whitney (BH) and AUROC of each metacluster share between classes.""" + from scipy import stats + from sklearn.metrics import roc_auc_score + + yy = y.reindex(shares.index) + ok = yy.notna() + shares = shares.loc[ok] + yy = yy.loc[ok].astype(int).to_numpy() + rows = [] + for col in shares.columns: + v = shares[col].to_numpy(dtype=float) + a, b = v[yy == 1], v[yy == 0] + both = a.size > 2 and b.size > 2 + rows.append( + dict( + meta=int(col[1:]), + share_pos=float(a.mean()) if a.size else float("nan"), + share_neg=float(b.mean()) if b.size else float("nan"), + present_pos=float((a > present_threshold).mean()) if a.size else float("nan"), + present_neg=float((b > present_threshold).mean()) if b.size else float("nan"), + auroc=float(roc_auc_score(yy, v)) if both else float("nan"), + p=float(stats.mannwhitneyu(a, b, alternative="two-sided").pvalue) + if both + else float("nan"), + ) + ) + T = pd.DataFrame(rows) + T["q"] = _bh(T["p"].to_numpy()) + return T.sort_values("p").reset_index(drop=True) + + +def _bh(p: np.ndarray) -> np.ndarray: + p = np.asarray(p, dtype=float) + q = np.full(p.size, np.nan) + ok = ~np.isnan(p) + if ok.sum(): + from scipy.stats import false_discovery_control + + q[ok] = false_discovery_control(p[ok], method="bh") + return q + + +def match_metaclusters( + records: pd.DataFrame, + *, + cell_type_names: Sequence[str], + cohort_frequency: np.ndarray, + distance: str = "hellinger", + threshold: Optional[float] = None, + scan_grid: Sequence[float] = tuple(np.linspace(0.15, 0.6, 46)), + min_folds: int = 3, + region_labels: Optional[Dict[str, np.ndarray]] = None, + region_fold: Optional[Dict[str, str]] = None, + patient_of: Optional[Dict[str, str]] = None, + labels_of: Optional[Dict[str, int]] = None, +) -> MatchResult: + records = records.reset_index(drop=True).copy() + comp_cols = [f"comp_{n}" for n in cell_type_names] + comp = records[comp_cols].to_numpy(dtype=float) + if distance == "hellinger": + D = hellinger_matrix(comp) + elif distance == "aitchison": + D = aitchison_matrix(comp) + else: + raise ValueError("distance must be hellinger or aitchison") + fold = records["fold"].to_numpy() + scan = None + if threshold is None: + threshold, groups, scan = scan_threshold(D, fold, grid=scan_grid, min_folds=min_folds) + else: + groups = constrained_average_linkage(D, fold, float(threshold)) + meta = np.zeros(len(records), dtype=int) + for k, g in enumerate(sorted(groups, key=lambda x: -len(x)), 1): + meta[g] = k + records["meta"] = meta + table = metacluster_table(records, D, cohort_frequency, cell_type_names) + result = MatchResult( + threshold=float(threshold), + distance=distance, + records=records, + table=table, + D=D, + scan=scan, + settings={ + "min_folds": min_folds, + "n_metaclusters": int(len(groups)), + "n_replicated": int(sum(len(g) >= min_folds for g in groups)), + }, + ) + if region_labels is not None and region_fold is not None: + id_to_meta = dict(zip(records["id"], records["meta"])) + pat_of = patient_of or {r: r for r in region_labels} + region_sh, pat_sh = patient_shares( + region_labels, region_fold, id_to_meta, pat_of, sorted(records["meta"].unique()) + ) + result.region_shares = region_sh + result.patient_shares = pat_sh + if labels_of is not None: + y_pat = pd.Series( + {pat_of.get(r, r): int(v) for r, v in labels_of.items() if r in region_labels} + ) + result.patient_tests = patient_tests(pat_sh, y_pat) + return result + + +def write_match_outputs( + result: MatchResult, + out_dir: str | Path, + *, + cell_type_names: Sequence[str], + figures: bool = True, + positive_name: str = "positive", +) -> Dict[str, str]: + out = Path(out_dir) + out.mkdir(parents=True, exist_ok=True) + paths: Dict[str, str] = {} + result.records.to_csv(out / "fold_clusters_matched.csv", index=False) + result.table.to_csv(out / "metaclusters.csv", index=False) + paths["metaclusters"] = str(out / "metaclusters.csv") + np.save(out / "fold_cluster_distances.npy", result.D) + if result.scan is not None: + result.scan.to_csv(out / "threshold_scan.csv", index=False) + if result.patient_shares is not None: + result.patient_shares.to_csv(out / "metacluster_patient_shares.csv") + result.region_shares.to_csv(out / "metacluster_region_shares.csv") + if result.patient_tests is not None: + result.patient_tests.to_csv(out / "metacluster_patient_tests.csv", index=False) + MT = result.table + md = [ + f"# Cross-fold niche replication: {result.distance} distance on composition, cannot-link within fold, threshold {result.threshold:.3f} -> {result.settings['n_metaclusters']} metaclusters, {result.settings['n_replicated']} replicated in >= {result.settings['min_folds']} folds ({len(result.records)} fold-clusters)", + "", + "| Meta | folds | share | regions (pos/neg) | composition | enriched | within-dist | emp. minus base, per fold | mean (sd) | margin per fold | mean (sd) | sign agree | CI excl 0 | emp/margin agree |", + "|---|---|---|---|---|---|---|---|---|---|---|---|---|---|", + ] + for _, r in MT.iterrows(): + md.append( + f"| M{r.meta} | {r.n_folds} ({r.folds}) | {100 * r.share_mean:.1f}% | {r.cores} ({r.pos_cores}/{r.neg_cores}) | {r.top_types} | {r.top_enriched} | {r.within_dist:.2f} | {r.emp_per_fold} | {r.emp_rel_mean:+.2f} ({r.emp_rel_sd:.2f}) | {r.margin_per_fold} | {r.margin_mean:+.2f} ({r.margin_sd:.2f}) | {r.sign_agree:.2f} | {r.ci_excl0}/{r.n_members} | {r.emp_margin_agree:.2f} |" + ) + if result.patient_tests is not None: + nf = dict(zip(MT.meta, MT.n_folds)) + md += [ + "", + f"## Patient-level metacluster shares (each region labelled once, by the model that held it out); {positive_name} vs rest", + "", + "| Meta | folds | share pos | share neg | present pos / neg | AUROC | p | q |", + "|---|---|---|---|---|---|---|---|", + ] + for _, r in result.patient_tests.iterrows(): + md.append( + f"| M{r.meta} | {nf.get(r.meta, '')} | {100 * r.share_pos:.2f}% | {100 * r.share_neg:.2f}% | {100 * r.present_pos:.0f}% / {100 * r.present_neg:.0f}% | {r.auroc:.3f} | {r.p:.3g} | {r.q:.2g} |" + ) + (out / "report.md").write_text("\n".join(md) + "\n") + paths["report"] = str(out / "report.md") + if figures: + try: + _overview_figure( + result, + out / "metaclusters_overview.png", + cell_type_names=cell_type_names, + positive_name=positive_name, + ) + paths["overview"] = str(out / "metaclusters_overview.png") + except Exception: # pragma: no cover - matplotlib missing + pass + return paths + + +def _overview_figure( + result: MatchResult, + path: Path, + *, + cell_type_names: Sequence[str], + positive_name: str, + max_types: int = 20, +) -> None: + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + MT = result.table + if MT.empty: + return + comp_cols = [f"comp_{n}" for n in cell_type_names] + cohort = MT[comp_cols].to_numpy(dtype=float).mean(0) + typ = np.argsort(-cohort)[:max_types] + H = MT[[comp_cols[j] for j in typ]].to_numpy(dtype=float) * 100 + m = len(MT) + folds = sorted(result.records["fold"].unique()) + cmap = plt.get_cmap("tab10") + fig, ax = plt.subplots( + 1, 3, figsize=(17, 0.5 * m + 2.4), gridspec_kw=dict(width_ratios=[2.6, 1.1, 1.3]) + ) + im = ax[0].imshow(H, aspect="auto", cmap="viridis", vmin=0, vmax=min(100, H.max())) + ax[0].set_xticks(range(len(typ))) + ax[0].set_xticklabels([cell_type_names[j] for j in typ], rotation=90, fontsize=7) + ax[0].set_yticks(range(m)) + ax[0].set_yticklabels( + [ + f"M{a} ({b} folds, {100 * s:.1f}%)" + for a, b, s in zip(MT.meta, MT.n_folds, MT.share_mean) + ], + fontsize=7, + ) + plt.colorbar(im, ax=ax[0], label="% of metacluster cells", fraction=0.03) + yy = np.arange(m)[::-1] + for k, (_, r) in enumerate(MT.iterrows()): + g = result.records[result.records["meta"] == r.meta] + for f, e, mg, lo, hi in zip( + g["fold"], g["emp_rel"], g["margin"], g["margin_lo"], g["margin_hi"] + ): + fi = folds.index(f) + off = (fi - len(folds) / 2) * 0.12 + ax[1].scatter(e, yy[k] + off, s=20, color=cmap(fi % 10), zorder=3) + ax[2].errorbar( + mg, + yy[k] + off, + xerr=[[mg - lo], [hi - mg]], + fmt="o", + ms=3.5, + color=cmap(fi % 10), + capsize=1.5, + lw=0.8, + ) + for a in ax[1:]: + a.axvline(0, color="k", lw=0.8) + a.set_yticks(yy) + a.set_yticklabels([]) + a.set_ylim(-0.7, m - 0.3) + a.spines["top"].set_visible(False) + a.spines["right"].set_visible(False) + ax[1].set_xlabel(f"empirical {positive_name} fraction minus fold base rate") + ax[2].set_xlabel(f"attention-weighted margin (+ = {positive_name})") + ax[1].set_title("association, one dot per fold", fontsize=9) + ax[2].set_title("attribution, one point per fold", fontsize=9) + for fi, f in enumerate(folds): + ax[2].scatter([], [], color=cmap(fi % 10), label=str(f)) + ax[2].legend(fontsize=7, loc="lower right") + fig.tight_layout() + fig.savefig(path, dpi=150) + plt.close(fig) diff --git a/src/grass_mil/interpretability/metaclusters/records.py b/src/grass_mil/interpretability/metaclusters/records.py new file mode 100644 index 0000000..91abca7 --- /dev/null +++ b/src/grass_mil/interpretability/metaclusters/records.py @@ -0,0 +1,183 @@ +"""Stage 1: cluster every fold model's held-out cells in its own space.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Dict, List, Optional, Sequence + +import numpy as np +import pandas as pd + +from grass_mil.inference.cell_cache import CellCache +from grass_mil.interpretability.niches.pipeline import NicheResult, run_niche_discovery + +__all__ = ["FoldClustering", "cluster_fold_models", "load_fold_clustering", "fold_cluster_records"] + + +@dataclass +class FoldClustering: + """Per-fold outputs needed by the later stages.""" + + records: pd.DataFrame # one row per fold-cluster (+ comp_* columns) + region_labels: Dict[str, np.ndarray] = field(default_factory=dict) # region -> cluster ids + region_fold: Dict[str, str] = field(default_factory=dict) # region -> fold model name + cell_type_names: List[str] = field(default_factory=list) + cohort_frequency: Optional[np.ndarray] = None + settings: Dict[str, Any] = field(default_factory=dict) + + +def fold_cluster_records(result: NicheResult, fold: str, base_rate: float) -> pd.DataFrame: + """Records of one fold's clusters from its niche table (noise excluded).""" + T = result.table[result.table["cluster"] != "Noise"].copy() + comp_cols = [c for c in T.columns if c.startswith("comp_")] + rows = [] + for _, t in T.iterrows(): + rows.append( + { + "fold": fold, + "cluster": int(t["cluster_id"]), + "id": f"{fold}_C{int(t['cluster_id'])}", + "share": float(t["share"]), + "n_cells": int(t["n_cells"]), + "cores": int(t["cores_present"]), + "pos_cores": int(t["pos_cores_present"]), + "neg_cores": int(t["neg_cores_present"]), + "emp": float(t["emp_frac_pos"]), + "emp_rel": float(t["emp_frac_pos"]) - float(base_rate), + "margin": float(t["relattn"]), + "margin_lo": float(t["relattn_lo"]), + "margin_hi": float(t["relattn_hi"]), + "lift": float(t["lift"]), + "share_auroc": float(t["share_auroc"]), + "top_types": t["top_types"], + "top_enriched": t["top_enriched"], + **{c: float(t[c]) for c in comp_cols}, + } + ) + return pd.DataFrame(rows) + + +def cluster_fold_models( + caches: Dict[str, CellCache], + *, + labels_of: Optional[Dict[str, int]] = None, + out_dir: Optional[str | Path] = None, + reduction: Optional[Dict[str, Any]] = None, + clustering: Optional[Dict[str, Any]] = None, + sample_size: Optional[int] = 100_000, + l2_normalise: bool = True, + assign_k: int = 15, + seed: int = 0, + n_boot: int = 2000, + positive_class: int = 1, + regions_of: Optional[Dict[str, Sequence[str]]] = None, + log=None, +) -> FoldClustering: + """Cluster each fold model's held-out regions (or ``regions_of[fold]``) with one + fixed setting; save ``labels_.npy``, ``cluster_model_.pkl`` and + the fold-cluster records.""" + import joblib + + out = Path(out_dir) if out_dir is not None else None + if out is not None: + out.mkdir(parents=True, exist_ok=True) + frames: List[pd.DataFrame] = [] + region_labels: Dict[str, np.ndarray] = {} + region_fold: Dict[str, str] = {} + names: List[str] = [] + all_ct: List[np.ndarray] = [] + per_fold_summary = {} + for fold, cache in caches.items(): + regions = ( + list(regions_of[fold]) + if regions_of and fold in regions_of + else (cache.held_out or cache.regions) + ) + res = run_niche_discovery( + cache, + regions=regions, + labels_of=labels_of, + space="gnn", + reduction=reduction, + clustering=clustering, + sample_size=sample_size, + l2_normalise=l2_normalise, + assign_k=assign_k, + seed=seed, + positive_class=positive_class, + n_boot=n_boot, + ) + base = float(res.y_cell.mean()) + frames.append(fold_cluster_records(res, fold, base)) + for r in res.cells.regions: + m = res.cells.region_ids == r + region_labels[r] = res.labels[m] + region_fold[r] = fold + names = res.cells.cell_type_names + all_ct.append(res.cells.ct) + per_fold_summary[fold] = { + "regions": len(res.cells.regions), + "cells": int(res.labels.size), + "clusters": res.n_clusters, + "noise": res.noise_fraction, + "base_rate": base, + **res.concordance, + } + if out is not None: + np.save(out / f"labels_{fold}.npy", res.labels) + joblib.dump(res.model, out / f"cluster_model_{fold}.pkl") + (out / f"regions_{fold}.json").write_text(json.dumps(list(res.cells.regions))) + if log is not None: + log.info( + f"[{fold}] {len(res.cells.regions)} regions, {res.labels.size} cells, {res.n_clusters} clusters, noise {res.noise_fraction:.2f}" + ) + records = pd.concat(frames, ignore_index=True) if frames else pd.DataFrame() + ct = np.concatenate(all_ct) if all_ct else np.zeros(0, dtype=int) + cohort = np.bincount(ct, minlength=len(names)) / max(1, ct.size) if names else None + fc = FoldClustering( + records=records, + region_labels=region_labels, + region_fold=region_fold, + cell_type_names=list(names), + cohort_frequency=cohort, + settings={ + "per_fold": per_fold_summary, + "reduction": reduction, + "clustering": clustering, + "sample_size": sample_size, + "l2_normalise": l2_normalise, + "assign_k": assign_k, + "seed": seed, + }, + ) + if out is not None: + records.to_csv(out / "fold_clusters.csv", index=False) + joblib.dump( + { + "region_labels": region_labels, + "region_fold": region_fold, + "cell_type_names": list(names), + "cohort_frequency": cohort, + "settings": fc.settings, + }, + out / "fold_clustering.pkl", + ) + return fc + + +def load_fold_clustering(out_dir: str | Path) -> FoldClustering: + import joblib + + out = Path(out_dir) + payload = joblib.load(out / "fold_clustering.pkl") + records = pd.read_csv(out / "fold_clusters.csv") + return FoldClustering( + records=records, + cell_type_names=payload["cell_type_names"], + cohort_frequency=payload["cohort_frequency"], + settings=payload["settings"], + region_labels=payload["region_labels"], + region_fold=payload["region_fold"], + ) diff --git a/src/grass_mil/interpretability/metaclusters/transfer.py b/src/grass_mil/interpretability/metaclusters/transfer.py new file mode 100644 index 0000000..ca5d673 --- /dev/null +++ b/src/grass_mil/interpretability/metaclusters/transfer.py @@ -0,0 +1,231 @@ +"""Stage 3: transfer every fold model's clustering to ALL regions in its own space. + +Each fold's saved cluster model (reducer + k-NN assigner fitted on its held-out +sample) labels every region's cells under that model's embedding; fold-cluster +ids map to metaclusters. Reports, per metacluster and model, share / +association / margin on TRAIN vs HELD-OUT regions, the pairwise ARI of the +models' metacluster labels on cells both assign, and consensus patient shares. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Dict, List, Optional, Sequence + +import numpy as np +import pandas as pd + +from grass_mil.inference.cell_cache import CellCache +from grass_mil.interpretability.niches.margins import relattn_margins +from grass_mil.interpretability.niches.pipeline import load_cells + +from .match import patient_tests + +__all__ = ["TransferResult", "run_transfer", "write_transfer_outputs"] + + +@dataclass +class TransferResult: + meta_labels: np.ndarray # (n_models, N_all) metacluster id or -1 + region_ids: np.ndarray # (N_all,) + models: List[str] + by_model: pd.DataFrame + ari: pd.DataFrame + consensus_region_shares: pd.DataFrame + consensus_patient_shares: Optional[pd.DataFrame] = None + consensus_patient_tests: Optional[pd.DataFrame] = None + refit_agreement: Dict[str, float] = field(default_factory=dict) + + +def run_transfer( + caches: Dict[str, CellCache], + *, + cluster_dir: str | Path, + matched_records: pd.DataFrame, + labels_of: Dict[str, int], + patient_of: Optional[Dict[str, str]] = None, + regions: Optional[Sequence[str]] = None, + positive_class: int = 1, + min_cells: int = 100, + present_threshold: float = 0.02, + log=None, +) -> TransferResult: + import joblib + from sklearn.metrics import adjusted_rand_score + + cluster_dir = Path(cluster_dir) + models = list(caches) + first = caches[models[0]] + regions = sorted(str(r) for r in (regions if regions is not None else first.regions)) + cells0 = load_cells(first, regions) + RID = cells0.region_ids + N = RID.size + y_cell = np.array([int(labels_of[r]) for r in RID]) + fold_of_region = {} + for name, cache in caches.items(): + for r in cache.held_out: + fold_of_region[str(r)] = name + split_model = np.array([fold_of_region.get(r, "") for r in RID], dtype=object) + meta_of = {(str(r.fold), int(r.cluster)): int(r.meta) for _, r in matched_records.iterrows()} + ML = np.full((len(models), N), -1, dtype=int) + MG = np.zeros((len(models), N), dtype=np.float32) + refit_agreement: Dict[str, float] = {} + for mi, name in enumerate(models): + cache = caches[name] + cells = load_cells(cache, regions) + if not np.array_equal(cells.region_ids, RID): + raise ValueError(f"Cache {name} does not cover the same cells as {models[0]}.") + model = joblib.load(cluster_dir / f"cluster_model_{name}.pkl") + labels_all = model.predict(cells.E) + ref_path = cluster_dir / f"labels_{name}.npy" + if ref_path.exists(): + held = split_model == name + ref = np.load(ref_path) + if ref.size == int(held.sum()): + refit_agreement[name] = float((labels_all[held] == ref).mean()) + labels_all[held] = ref # the held-out cells keep their stage-1 labels exactly + ML[mi] = np.array([meta_of.get((name, int(c)), -1) if c >= 0 else -1 for c in labels_all]) + rel = relattn_margins(cells.LG, cells.AR, cells.bias, RID, positive_class=positive_class) + MG[mi] = rel.margin + if log is not None: + log.info( + f"[{name}] transferred to {len(regions)} regions; refit agreement {refit_agreement.get(name, float('nan')):.3f}" + ) + metas = sorted(set(matched_records["meta"].astype(int))) + rows = [] + for k in metas: + for mi, name in enumerate(models): + mk = ML[mi] == k + if mk.sum() < int(min_cells): + continue + for split_name, sm in ( + ("train", split_model != name), + ("heldout", split_model == name), + ): + ms = mk & sm + if not sm.any(): + continue + base = float(y_cell[sm].mean()) + present = [ + r + for r in regions + if ((split_model[RID == r][0] == name) == (split_name == "heldout")) + and ms[RID == r].mean() >= present_threshold + ] + vals = [ + float(MG[mi][ms & (RID == r)].mean()) + for r in present + if (ms & (RID == r)).any() + ] + rows.append( + dict( + meta=k, + model=name, + split=split_name, + share=float(ms.sum() / sm.sum()), + cores=len(present), + pos_cores=int(sum(labels_of[r] for r in present)), + emp=float(y_cell[ms].mean()) if ms.any() else float("nan"), + emp_rel=(float(y_cell[ms].mean()) - base) if ms.any() else float("nan"), + margin=float(np.mean(vals)) if vals else float("nan"), + ) + ) + by_model = pd.DataFrame(rows) + ari = pd.DataFrame(np.nan, index=models, columns=models) + for i, a in enumerate(models): + for j, b in enumerate(models): + both = (ML[i] >= 0) & (ML[j] >= 0) + if both.sum() > 10: + ari.loc[a, b] = adjusted_rand_score(ML[i][both], ML[j][both]) + region_shares = pd.DataFrame( + { + f"M{k}": [ + float(np.mean([np.mean(ML[i][RID == r] == k) for i in range(len(models))])) + for r in regions + ] + for k in metas + }, + index=regions, + ) + region_shares.index.name = "region_id" + result = TransferResult( + meta_labels=ML, + region_ids=RID, + models=models, + by_model=by_model, + ari=ari, + consensus_region_shares=region_shares, + refit_agreement=refit_agreement, + ) + pat_of = patient_of or {r: r for r in regions} + pat = region_shares.copy() + pat["patient_id"] = [pat_of.get(r, r) for r in regions] + result.consensus_patient_shares = pat.groupby("patient_id").mean() + y_pat = pd.Series({pat_of.get(r, r): int(labels_of[r]) for r in regions}) + result.consensus_patient_tests = patient_tests( + result.consensus_patient_shares, y_pat, present_threshold=0.005 + ) + return result + + +def write_transfer_outputs( + result: TransferResult, + out_dir: str | Path, + *, + metaclusters: pd.DataFrame, + positive_name: str = "positive", +) -> Dict[str, str]: + out = Path(out_dir) + out.mkdir(parents=True, exist_ok=True) + np.save(out / "transfer_metalabels.npy", result.meta_labels) + pd.DataFrame({"region_id": result.region_ids}).to_csv(out / "transfer_cells.csv", index=False) + result.by_model.to_csv(out / "transfer_meta_by_model.csv", index=False) + result.ari.to_csv(out / "transfer_model_ari.csv") + result.consensus_region_shares.to_csv(out / "transfer_region_shares.csv") + if result.consensus_patient_shares is not None: + result.consensus_patient_shares.to_csv(out / "transfer_patient_shares.csv") + if result.consensus_patient_tests is not None: + result.consensus_patient_tests.to_csv(out / "transfer_patient_tests.csv", index=False) + MT = metaclusters.set_index("meta") + models = result.models + pairs = [(a, b) for i, a in enumerate(models) for b in models[i + 1 :]] + md = [ + "# Metaclusters transferred to all regions by each fold model", + "", + "Stage-1 refit agreement (held-out cells relabelled by the saved model): " + + "; ".join(f"{k} {v:.3f}" for k, v in result.refit_agreement.items()), + "", + "Pairwise ARI of metacluster labels between models on cells both assign: " + + "; ".join(f"{a}-{b} {result.ari.loc[a, b]:.2f}" for a, b in pairs), + "", + "| Meta | folds | composition | model | share train / held-out | regions train (pos) / held-out (pos) | emp-base train / held-out | margin train / held-out |", + "|---|---|---|---|---|---|---|---|", + ] + for k in MT.index: + g = result.by_model[result.by_model["meta"] == k] + first = True + for m in models: + a = g[(g.model == m) & (g.split == "train")] + h = g[(g.model == m) & (g.split == "heldout")] + if a.empty or h.empty: + continue + a, h = a.iloc[0], h.iloc[0] + md.append( + f"| {'M' + str(k) if first else ''} | {MT.loc[k, 'n_folds'] if first else ''} | {MT.loc[k, 'top_types'] if first else ''} | {m} | {100 * a.share:.2f}% / {100 * h.share:.2f}% | {a.cores} ({a.pos_cores}) / {h.cores} ({h.pos_cores}) | {a.emp_rel:+.2f} / {h.emp_rel:+.2f} | {a.margin:+.2f} / {h.margin:+.2f} |" + ) + first = False + if result.consensus_patient_tests is not None: + md += [ + "", + "## Consensus patient-level shares (mean over the models' assignments of every region)", + "", + "| Meta | folds | composition | share pos | share neg | present pos / neg | AUROC | p | q |", + "|---|---|---|---|---|---|---|---|---|", + ] + for _, r in result.consensus_patient_tests.iterrows(): + md.append( + f"| M{r.meta} | {MT.loc[r.meta, 'n_folds'] if r.meta in MT.index else ''} | {MT.loc[r.meta, 'top_types'] if r.meta in MT.index else ''} | {100 * r.share_pos:.2f}% | {100 * r.share_neg:.2f}% | {100 * r.present_pos:.0f}% / {100 * r.present_neg:.0f}% | {r.auroc:.3f} | {r.p:.3g} | {r.q:.2g} |" + ) + (out / "report_transfer.md").write_text("\n".join(md) + "\n") + return {"report": str(out / "report_transfer.md")} diff --git a/src/grass_mil/interpretability/metaclusters_cli.py b/src/grass_mil/interpretability/metaclusters_cli.py new file mode 100644 index 0000000..7e7aed1 --- /dev/null +++ b/src/grass_mil/interpretability/metaclusters_cli.py @@ -0,0 +1,187 @@ +"""``grass-mil-metaclusters``: cross-fold niche replication (cluster / match / transfer / basis).""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Dict, List, Optional + +import hydra +import numpy as np +import pandas as pd +from omegaconf import DictConfig, OmegaConf + +from grass_mil._paths import setup_project_root + +setup_project_root() + +from grass_mil.inference.cell_cache import CellCache # noqa: E402 +from grass_mil.interpretability.metaclusters.basis import run_shared_basis, write_basis_outputs # noqa: E402 +from grass_mil.interpretability.metaclusters.match import match_metaclusters, write_match_outputs # noqa: E402 +from grass_mil.interpretability.metaclusters.records import ( + cluster_fold_models, + load_fold_clustering, +) # noqa: E402 +from grass_mil.interpretability.metaclusters.transfer import run_transfer, write_transfer_outputs # noqa: E402 +from grass_mil.utils import RankedLogger # noqa: E402 + +log = RankedLogger(__name__, rank_zero_only=True) + +__all__ = ["run_metaclusters", "main", "discover_caches", "load_labels_and_patients"] + + +def discover_caches( + cells_dir: str | Path, models: Optional[List[str]] = None +) -> Dict[str, CellCache]: + root = Path(cells_dir) + if (root / "index.json").exists(): + c = CellCache.open(root) + return {c.index.get("model_name", root.name): c} + found: Dict[str, CellCache] = {} + for sub in sorted(p for p in root.iterdir() if p.is_dir() and (p / "index.json").exists()): + c = CellCache.open(sub) + name = c.index.get("model_name", sub.name) + if models and name not in models: + continue + found[name] = c + if not found: + raise FileNotFoundError(f"No cell caches under {root}") + return found + + +def load_labels_and_patients(cfg_labels): + df = pd.read_csv(str(cfg_labels.label_file)) + df.columns = [str(c).strip() for c in df.columns] + idc = str(cfg_labels.id_column) + lc = str(cfg_labels.label_column) + df = df.dropna(subset=[lc]) + labels_of = {str(r): int(float(v) >= 0.5) for r, v in zip(df[idc], df[lc])} + pc = cfg_labels.get("patient_column") + patient_of = {str(r): str(p) for r, p in zip(df[idc], df[pc])} if pc else None + return labels_of, patient_of + + +def run_metaclusters(cfg: DictConfig) -> Dict[str, Any]: + out = Path(str(cfg.output_dir)) + out.mkdir(parents=True, exist_ok=True) + caches = discover_caches(str(cfg.cells_dir), list(cfg.models) if cfg.get("models") else None) + held = {k: v for k, v in caches.items() if v.held_out} + if not held: + raise ValueError( + "No cache records held-out regions; extract from a nested-CV summary (or +extract.held_out)." + ) + caches = held + labels_of, patient_of = load_labels_and_patients(cfg.labels) + stage = str(cfg.get("stage", "all")).lower() + stages = ["cluster", "match", "transfer", "basis"] if stage == "all" else [stage] + cluster_dir = out / "cluster" + match_dir = out / "match" + summary: Dict[str, Any] = {"models": list(caches)} + names = None + cohort = None + fc = None + if "cluster" in stages: + fc = cluster_fold_models( + caches, + labels_of=labels_of, + out_dir=cluster_dir, + reduction=OmegaConf.to_container(cfg.reduction, resolve=True), + clustering=OmegaConf.to_container(cfg.clustering, resolve=True), + sample_size=cfg.get("sample_size"), + l2_normalise=bool(cfg.get("l2_normalise", True)), + assign_k=int(cfg.get("assign_k", 15)), + seed=int(cfg.get("seed", 0)), + n_boot=int(cfg.get("n_boot", 2000)), + positive_class=int(cfg.get("positive_class", 1)), + log=log, + ) + summary["cluster"] = fc.settings["per_fold"] + if "match" in stages or "transfer" in stages or "basis" in stages: + if fc is None: + fc = load_fold_clustering(cluster_dir) + names = fc.cell_type_names + cohort = fc.cohort_frequency + matched = None + if "match" in stages: + m = cfg.match + grid = np.linspace(float(m.scan.min), float(m.scan.max), int(m.scan.n)) + res = match_metaclusters( + fc.records, + cell_type_names=names, + cohort_frequency=cohort, + distance=str(m.distance), + threshold=m.get("threshold"), + scan_grid=grid, + min_folds=int(m.min_folds), + region_labels=fc.region_labels, + region_fold=fc.region_fold, + patient_of=patient_of, + labels_of=labels_of, + ) + paths = write_match_outputs( + res, + match_dir, + cell_type_names=names, + figures=bool(cfg.get("figures", True)), + positive_name=str(cfg.positive_name), + ) + matched = res.records + summary["match"] = {"threshold": res.threshold, **res.settings, "report": paths["report"]} + log.info( + f"match: threshold {res.threshold:.3f}, {res.settings['n_metaclusters']} metaclusters, {res.settings['n_replicated']} replicated" + ) + if "transfer" in stages: + if matched is None: + matched = pd.read_csv(match_dir / "fold_clusters_matched.csv") + mt = pd.read_csv(match_dir / "metaclusters.csv") + res_t = run_transfer( + caches, + cluster_dir=cluster_dir, + matched_records=matched, + labels_of=labels_of, + patient_of=patient_of, + positive_class=int(cfg.get("positive_class", 1)), + min_cells=int(cfg.transfer.min_cells), + present_threshold=float(cfg.transfer.present_threshold), + log=log, + ) + paths = write_transfer_outputs( + res_t, out / "transfer", metaclusters=mt, positive_name=str(cfg.positive_name) + ) + summary["transfer"] = {"refit_agreement": res_t.refit_agreement, "report": paths["report"]} + if "basis" in stages and bool(cfg.basis.get("enabled", True)): + b = cfg.basis + basis_cache = None + sm = b.get("space_model") + if sm: + basis_cache = caches.get(str(sm)) or CellCache.open(str(sm)) + res_b = run_shared_basis( + caches, + basis_cache=basis_cache, + labels_of=labels_of, + k=int(b.k), + sample_size=int(b.sample_size), + seed=int(b.seed), + n_init=int(b.n_init), + positive_class=int(cfg.get("positive_class", 1)), + n_boot=int(cfg.get("n_boot", 2000)), + log=log, + ) + paths = write_basis_outputs( + res_b, + out / "basis", + positive_name=str(cfg.positive_name), + figures=bool(cfg.get("figures", True)), + ) + summary["basis"] = {"k": int(b.k), "report": paths["report"]} + (out / "metaclusters_summary.json").write_text(json.dumps(summary, indent=1, default=str)) + return summary + + +@hydra.main(version_base="1.3", config_path="../configs", config_name="metaclusters.yaml") +def main(cfg: DictConfig) -> None: + run_metaclusters(cfg) + + +if __name__ == "__main__": + main() diff --git a/src/grass_mil/interpretability/niches/__init__.py b/src/grass_mil/interpretability/niches/__init__.py new file mode 100644 index 0000000..654aa8a --- /dev/null +++ b/src/grass_mil/interpretability/niches/__init__.py @@ -0,0 +1,16 @@ +"""Niche discovery on a trained GRASS-MIL model's per-cell cache.""" + +from .colocalisation import cluster_colocalisation +from .margins import attention_space_from_checkpoint, relattn_margins +from .pipeline import NicheResult, run_niche_discovery +from .tables import niche_table, spearman_concordance + +__all__ = [ + "NicheResult", + "attention_space_from_checkpoint", + "cluster_colocalisation", + "niche_table", + "relattn_margins", + "run_niche_discovery", + "spearman_concordance", +] diff --git a/src/grass_mil/interpretability/niches/colocalisation.py b/src/grass_mil/interpretability/niches/colocalisation.py new file mode 100644 index 0000000..0534e25 --- /dev/null +++ b/src/grass_mil/interpretability/niches/colocalisation.py @@ -0,0 +1,34 @@ +"""Niche-niche co-localisation on the cell graph: log2(observed / expected) edge counts.""" + +from __future__ import annotations + +from typing import List, Tuple + +import numpy as np + +__all__ = ["cluster_colocalisation"] + + +def cluster_colocalisation( + labels: np.ndarray, edge_index: np.ndarray +) -> Tuple[np.ndarray, List[int]]: + """``log2((observed + 1) / (E + 1))`` with ``E_ab = d_a d_b / sum(d)`` from the + symmetrised cluster-cluster edge count matrix ``observed`` (noise cells ignored).""" + labels = np.asarray(labels).astype(int) + ei = np.asarray(edge_index).astype(int) + la, lb = labels[ei[0]], labels[ei[1]] + keep = (la >= 0) & (lb >= 0) + la, lb = la[keep], lb[keep] + clusters = sorted(set(labels.tolist()) - {-1}) + idx = {c: i for i, c in enumerate(clusters)} + n = len(clusters) + observed = np.zeros((n, n)) + if la.size: + ia = np.vectorize(idx.get)(la) + ib = np.vectorize(idx.get)(lb) + np.add.at(observed, (ia, ib), 1) + observed = observed + observed.T + deg = observed.sum(axis=1) + total = deg.sum() + expected = np.outer(deg, deg) / total if total > 0 else np.zeros_like(observed) + return np.log2((observed + 1.0) / (expected + 1.0)), clusters diff --git a/src/grass_mil/interpretability/niches/figures.py b/src/grass_mil/interpretability/niches/figures.py new file mode 100644 index 0000000..d86f9a1 --- /dev/null +++ b/src/grass_mil/interpretability/niches/figures.py @@ -0,0 +1,180 @@ +"""Figures of the niche report (matplotlib; UMAP optional).""" + +from __future__ import annotations + +from pathlib import Path +from typing import Optional, Sequence + +import numpy as np +import pandas as pd + +__all__ = ["embedding_triptych", "clusters_overview", "colocalisation_heatmap", "umap_coordinates"] + + +def _plt(): + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + return plt + + +def umap_coordinates( + P: np.ndarray, + *, + n_neighbors: int = 30, + min_dist: float = 0.1, + metric: str = "cosine", + seed: int = 0, +) -> Optional[np.ndarray]: + try: + import umap # type: ignore + except Exception: + return None + return umap.UMAP( + n_neighbors=int(n_neighbors), + min_dist=float(min_dist), + metric=str(metric), + random_state=int(seed), + ).fit_transform(P) + + +def embedding_triptych( + coords: np.ndarray, + labels: np.ndarray, + margin: np.ndarray, + y_cell: np.ndarray, + path: Path, + *, + title: str, + axis_labels: Sequence[str] = ("dim 1", "dim 2"), + positive_name: str = "positive", + margin_clip: float = 3.0, +) -> Path: + """Clusters | attention-weighted margin | empirical association, side by side.""" + plt = _plt() + clusters = sorted(set(labels.tolist()) - {-1}) + cmap = plt.get_cmap("tab20") + fig, ax = plt.subplots(1, 3, figsize=(15, 4.6)) + noise = labels < 0 + ax[0].scatter(coords[noise, 0], coords[noise, 1], s=1, c="#cccccc", lw=0, label="noise") + for i, c in enumerate(clusters): + mm = labels == c + ax[0].scatter(coords[mm, 0], coords[mm, 1], s=1.5, color=cmap(i % 20), lw=0, label=f"C{c}") + ax[0].legend(markerscale=8, fontsize=6, ncol=2, loc="best") + ax[0].set_title(f"{title}: clusters ({len(clusters)}, noise {noise.mean():.0%})", fontsize=9) + mv = np.clip(margin, -margin_clip, margin_clip) + sc = ax[1].scatter( + coords[:, 0], + coords[:, 1], + s=1.5, + c=mv, + cmap="RdBu_r", + vmin=-margin_clip, + vmax=margin_clip, + lw=0, + ) + plt.colorbar(sc, ax=ax[1], label=f"relattn margin (+ = {positive_name})") + ax[1].set_title("attention-weighted margin", fontsize=9) + hb = ax[2].hexbin( + coords[:, 0], coords[:, 1], C=y_cell, gridsize=45, cmap="RdBu_r", vmin=0, vmax=1, mincnt=5 + ) + plt.colorbar(hb, ax=ax[2], label=f"fraction of cells from {positive_name} regions") + ax[2].set_title("empirical association", fontsize=9) + for a in ax: + a.set_xlabel(axis_labels[0]) + a.set_ylabel(axis_labels[1]) + a.set_xticks([]) + a.set_yticks([]) + fig.tight_layout() + fig.savefig(path, dpi=150) + plt.close(fig) + return path + + +def clusters_overview( + table: pd.DataFrame, + path: Path, + *, + cell_type_names: Sequence[str], + cohort_frequency: np.ndarray, + base_rate: float, + positive_name: str = "positive", + max_types: int = 20, +) -> Path: + """Composition heatmap | empirical association | margin with CI, per cluster.""" + plt = _plt() + tt = table[table["cluster"] != "Noise"].sort_values("emp_frac_pos", ascending=False) + if tt.empty: + return path + typ = np.argsort(-np.asarray(cohort_frequency))[:max_types] + comp_cols = [f"comp_{cell_type_names[j]}" for j in typ] + H = tt[comp_cols].to_numpy(dtype=float) * 100 + fig, ax = plt.subplots( + 1, 3, figsize=(16, 0.42 * len(tt) + 2.2), gridspec_kw=dict(width_ratios=[2.6, 1, 1.3]) + ) + im = ax[0].imshow( + H, aspect="auto", cmap="viridis", vmin=0, vmax=min(100, H.max() if H.size else 100) + ) + ax[0].set_xticks(range(len(typ))) + ax[0].set_xticklabels([cell_type_names[j] for j in typ], rotation=90, fontsize=7) + ax[0].set_yticks(range(len(tt))) + ax[0].set_yticklabels( + [ + f"{c} ({100 * s:.1f}%, {n} regions)" + for c, s, n in zip(tt.cluster, tt.share, tt.cores_present) + ], + fontsize=7, + ) + plt.colorbar(im, ax=ax[0], label="% of cluster cells", fraction=0.03) + yy = np.arange(len(tt))[::-1] + ax[1].barh( + yy, + tt.emp_frac_pos, + color=["#b2182b" if v > base_rate else "#2166ac" for v in tt.emp_frac_pos], + height=0.6, + ) + ax[1].axvline(base_rate, color="k", ls=":") + ax[1].set_yticks([]) + ax[1].set_xlabel(f"fraction of cells from {positive_name} regions") + ax[1].set_xlim(0, 1) + ax[2].errorbar( + tt.relattn, + yy, + xerr=[tt.relattn - tt.relattn_lo, tt.relattn_hi - tt.relattn], + fmt="o", + ms=4, + color="#1b9e77", + capsize=2, + ) + ax[2].axvline(0, color="k", lw=0.8) + ax[2].set_yticks([]) + ax[2].set_xlabel(f"attention-weighted margin (+ = {positive_name})") + for a in ax[1:]: + a.spines["top"].set_visible(False) + a.spines["right"].set_visible(False) + fig.tight_layout() + fig.savefig(path, dpi=150) + plt.close(fig) + return path + + +def colocalisation_heatmap(CO: np.ndarray, clusters: Sequence[int], path: Path) -> Path: + plt = _plt() + n = len(clusters) + if n == 0: + return path + fig, ax = plt.subplots(figsize=(0.45 * n + 2, 0.45 * n + 1.5)) + v = float(np.nanmax(np.abs(CO))) if np.isfinite(CO).any() else 1.0 + im = ax.imshow(CO, cmap="RdBu_r", vmin=-v, vmax=v) + ax.set_xticks(range(n)) + ax.set_xticklabels([f"C{c}" for c in clusters], rotation=90, fontsize=7) + ax.set_yticks(range(n)) + ax.set_yticklabels([f"C{c}" for c in clusters], fontsize=7) + plt.colorbar(im, ax=ax, label="log2 obs/exp cell-cell edges") + ax.set_title("cluster co-localisation", fontsize=9) + fig.tight_layout() + fig.savefig(path, dpi=150) + plt.close(fig) + return path diff --git a/src/grass_mil/interpretability/niches/margins.py b/src/grass_mil/interpretability/niches/margins.py new file mode 100644 index 0000000..ffc48d6 --- /dev/null +++ b/src/grass_mil/interpretability/niches/margins.py @@ -0,0 +1,108 @@ +"""Per-cell attention-weighted margins and the attention feature space.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Sequence + +import numpy as np + +__all__ = ["RelattnResult", "relattn_margins", "attention_space_from_checkpoint"] + + +@dataclass +class RelattnResult: + margin: np.ndarray # (N,) N_r * [A1 (l1 - b1) - A0 (l0 - b0)] (binary) + attention: np.ndarray # (N, C) attention normalised within region per class + contributions: np.ndarray # (N, C) A_c * (l_c - b_c) + + +def relattn_margins( + instance_logits: np.ndarray, + attention_logits: np.ndarray, + bias: Sequence[float], + region_ids: np.ndarray, + *, + positive_class: int = 1, +) -> RelattnResult: + """Attention-weighted, region-size-scaled cell margins (``relattn``). + + Attention is a softmax over the region's cells per class, so a cell's + contribution ``A[i,c] (l[i,c] - b_c)`` sums over the region to the bag + logit minus bias. Multiplying by the region's cell count ``N_r`` makes the + quantity comparable across regions of different size (a uniformly attended + cell gets weight 1). For a binary head the margin contrasts the positive + class with class 0; for more classes it is one-vs-rest. + """ + LG = np.asarray(instance_logits, dtype=np.float64) + AR = np.asarray(attention_logits, dtype=np.float64) + b = np.asarray(bias, dtype=np.float64).reshape(-1) + rid = np.asarray(region_ids) + if LG.shape != AR.shape: + raise ValueError(f"instance_logits {LG.shape} and attention_logits {AR.shape} differ") + n, c = LG.shape + if b.size != c: + raise ValueError(f"bias has {b.size} entries for {c} classes") + A = np.zeros_like(AR) + scale = np.zeros(n, dtype=np.float64) + for r in np.unique(rid): + m = rid == r + ar = AR[m] + a = np.exp(ar - ar.max(axis=0, keepdims=True)) + a /= a.sum(axis=0, keepdims=True) + A[m] = a + scale[m] = float(m.sum()) + contrib = A * (LG - b[None, :]) + pos = int(positive_class) + if c == 1: + margin = contrib[:, 0] + elif c == 2: + margin = contrib[:, pos] - contrib[:, 1 - pos] + else: + rest = (contrib.sum(axis=1) - contrib[:, pos]) / (c - 1) + margin = contrib[:, pos] - rest + return RelattnResult( + margin=(margin * scale).astype(np.float32), + attention=A.astype(np.float32), + contributions=contrib.astype(np.float32), + ) + + +def attention_space_from_checkpoint( + embeddings: np.ndarray, ckpt_path: str, *, batch_size: int = 65536 +) -> np.ndarray: + """The attention head's pre-tanh activation for every cell, from a checkpoint. + + Rebuilds the projected gated attention head from the ``attention.*`` weights + (dimensions inferred from the tensors) and applies ``attention_space``. + """ + import torch + + from grass_mil.inference.model_loading import read_checkpoint_state + from grass_mil.models.components.attention import AttnNetGatedProjected + + state = read_checkpoint_state(ckpt_path) + att = {k[len("attention.") :]: v for k, v in state.items() if k.startswith("attention.")} + if "projection.0.weight" not in att: + raise ValueError( + f"{ckpt_path} has no projected gated attention (attention.projection.0.weight)." + ) + proj_dim, in_dim = att["projection.0.weight"].shape + hidden = att["attention_a.0.weight"].shape[0] + n_classes = att["attention_c.weight"].shape[0] + module = AttnNetGatedProjected( + input_dim=int(in_dim), + projection_dim=int(proj_dim), + hidden_dim=int(hidden), + n_classes=int(n_classes), + ) + module.load_state_dict(att, strict=True) + module.eval() + out = [] + E = np.asarray(embeddings, dtype=np.float32) + with torch.no_grad(): + for i in range(0, E.shape[0], int(batch_size)): + out.append( + module.attention_space(torch.from_numpy(E[i : i + int(batch_size)])).numpy() + ) + return np.concatenate(out) if out else np.zeros((0, int(hidden)), dtype=np.float32) diff --git a/src/grass_mil/interpretability/niches/pipeline.py b/src/grass_mil/interpretability/niches/pipeline.py new file mode 100644 index 0000000..ef7eb77 --- /dev/null +++ b/src/grass_mil/interpretability/niches/pipeline.py @@ -0,0 +1,372 @@ +"""Niche discovery on one model's cell cache. + +Steps (preprint): take the cells of the chosen regions (held-out by default), +compute the relattn margin of every cell, cluster the chosen feature space +(encoder embedding or the attention head's 8-d space) with the user's +reduction + clustering settings (default cosine HDBSCAN: PCA-10 on a 100k +sample, L2, HDBSCAN mcs 300 / min_samples 1 / eom, 15-NN assignment), then +describe every cluster (composition, enrichment, empirical association, +margin with bootstrap CI over regions, share AUROC, co-localisation) and +draw the figures. +""" + +from __future__ import annotations + +import json +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Dict, List, Optional, Sequence + +import numpy as np +import pandas as pd + +from grass_mil.inference.cell_cache import CellCache +from grass_mil.interpretability.core.clustering import NicheClusterModel, fit_niche_clustering + +from .colocalisation import cluster_colocalisation +from .margins import attention_space_from_checkpoint, relattn_margins +from .tables import niche_table, spearman_concordance + +__all__ = ["NicheResult", "load_cells", "run_niche_discovery", "write_niche_outputs"] + +DEFAULT_REDUCTION = {"method": "pca", "n_components": 10} +DEFAULT_CLUSTERING = { + "method": "hdbscan", + "params": {"min_cluster_size": 300, "min_samples": 1, "cluster_selection_method": "eom"}, +} + + +@dataclass +class CellSet: + regions: List[str] + region_ids: np.ndarray # (N,) + E: np.ndarray # (N, d) float32 + LG: np.ndarray + AR: Optional[np.ndarray] + ct: np.ndarray + pos: np.ndarray + edge_index: np.ndarray # (2, M) global indices + bias: np.ndarray + cell_type_names: List[str] + + +def load_cells(cache: CellCache, regions: Sequence[str]) -> CellSet: + """Concatenate the cached arrays of ``regions`` (in the given order).""" + E, LG, AR, CT, POS, EDGES, RID = [], [], [], [], [], [], [] + off = 0 + for r in regions: + d = cache.load(r) + n = int(d["E"].shape[0]) + E.append(d["E"].astype(np.float32)) + LG.append(d["LG"]) + AR.append(d["AR"] if "AR" in d else None) + CT.append(d["ct"].astype(int) if "ct" in d else np.zeros(n, dtype=int)) + POS.append(d["pos"]) + EDGES.append(d["edge_index"].astype(np.int64) + off) + RID.append(np.full(n, str(r), dtype=object)) + off += n + if not E: + raise ValueError("No regions to load.") + names = cache.cell_type_names + ct = np.concatenate(CT) + if names is None: + names = [str(i) for i in range(int(ct.max()) + 1 if ct.size else 0)] + return CellSet( + regions=list(regions), + region_ids=np.concatenate(RID), + E=np.concatenate(E), + LG=np.concatenate(LG), + AR=np.concatenate(AR) if all(a is not None for a in AR) else None, + ct=ct, + pos=np.concatenate(POS), + edge_index=np.concatenate(EDGES, axis=1), + bias=cache.bias, + cell_type_names=list(names), + ) + + +@dataclass +class NicheResult: + labels: np.ndarray + model: NicheClusterModel + table: pd.DataFrame + margin: np.ndarray + attention: np.ndarray + colocalisation: np.ndarray + clusters: List[int] + concordance: Dict[str, float] + cells: CellSet + features: np.ndarray + settings: Dict[str, Any] = field(default_factory=dict) + y_cell: Optional[np.ndarray] = None + + @property + def n_clusters(self) -> int: + return len(self.clusters) + + @property + def noise_fraction(self) -> float: + return float((self.labels == -1).mean()) if self.labels.size else float("nan") + + +def _region_labels( + cache: CellCache, regions: Sequence[str], labels_of: Optional[Dict[str, int]] +) -> Dict[str, int]: + out: Dict[str, int] = {} + for r in regions: + if labels_of is not None and r in labels_of: + out[r] = int(labels_of[r]) + else: + v = cache.label_of(r) + if v is None: + raise ValueError(f"No label for region {r}: pass labels_of or a labelled cache.") + out[r] = int(round(float(v))) + return out + + +def run_niche_discovery( + cache: CellCache, + *, + regions: Optional[Sequence[str]] = None, + labels_of: Optional[Dict[str, int]] = None, + space: str = "gnn", + attention_ckpt: Optional[str] = None, + reduction: Optional[Dict[str, Any]] = None, + clustering: Optional[Dict[str, Any]] = None, + sample_size: Optional[int] = 100_000, + l2_normalise: bool = True, + assign_k: int = 15, + seed: int = 0, + positive_class: int = 1, + present_threshold: float = 0.02, + n_boot: int = 2000, +) -> NicheResult: + if regions is None: + regions = cache.held_out or cache.regions + regions = sorted(str(r) for r in regions) + cells = load_cells(cache, regions) + region_labels = _region_labels(cache, regions, labels_of) + if cells.AR is None: + raise ValueError( + "The cache has no attention logits (AR); niche margins need an attention model." + ) + rel = relattn_margins( + cells.LG, cells.AR, cells.bias, cells.region_ids, positive_class=positive_class + ) + space = str(space).lower() + if space in {"gnn", "encoder", "embedding"}: + X = cells.E + elif space in {"attention", "attn"}: + if not attention_ckpt: + raise ValueError("space='attention' needs attention_ckpt (the model checkpoint).") + X = attention_space_from_checkpoint(cells.E, attention_ckpt) + else: + raise ValueError("space must be 'gnn' or 'attention'") + model, labels = fit_niche_clustering( + X, + reduction=reduction or DEFAULT_REDUCTION, + clustering=clustering or DEFAULT_CLUSTERING, + sample_size=sample_size, + l2_normalise=l2_normalise, + assign_k=assign_k, + seed=seed, + ) + table = niche_table( + labels, + cell_types=cells.ct, + region_ids=cells.region_ids, + region_labels=region_labels, + margin=rel.margin, + attention_pos=rel.attention[:, min(positive_class, rel.attention.shape[1] - 1)], + cell_type_names=cells.cell_type_names, + present_threshold=present_threshold, + n_boot=n_boot, + seed=seed, + ) + CO, clusters = cluster_colocalisation(labels, cells.edge_index) + y_cell = np.array([region_labels[r] for r in cells.region_ids]) + return NicheResult( + labels=labels, + model=model, + table=table, + margin=rel.margin, + attention=rel.attention, + colocalisation=CO, + clusters=clusters, + concordance=spearman_concordance(table), + cells=cells, + features=X, + settings={ + "space": space, + "regions": regions, + "n_cells": int(labels.size), + "positive_class": int(positive_class), + **model.params, + }, + y_cell=y_cell, + ) + + +def write_niche_outputs( + result: NicheResult, + out_dir: str | Path, + *, + title: str = "niche discovery", + positive_name: str = "positive", + figures: bool = True, + umap: Optional[Dict[str, Any]] = None, + held_out_auroc: Optional[float] = None, +) -> Dict[str, str]: + """Write labels, cluster model, tables, report and figures; returns the paths.""" + import joblib + + out = Path(out_dir) + out.mkdir(parents=True, exist_ok=True) + paths: Dict[str, str] = {} + np.save(out / "labels.npy", result.labels) + paths["labels"] = str(out / "labels.npy") + joblib.dump(result.model, out / "cluster_model.pkl") + paths["cluster_model"] = str(out / "cluster_model.pkl") + cells = pd.DataFrame( + { + "region_id": result.cells.region_ids, + "cell_index": np.concatenate( + [np.arange((result.cells.region_ids == r).sum()) for r in result.cells.regions] + ), + "cluster": result.labels, + "cell_type": [result.cells.cell_type_names[c] for c in result.cells.ct], + "relattn": result.margin, + "x": result.cells.pos[:, 0], + "y": result.cells.pos[:, 1], + } + ) + cells.to_csv(out / "cells.csv", index=False) + paths["cells"] = str(out / "cells.csv") + result.table.to_csv(out / "clusters.csv", index=False) + paths["clusters"] = str(out / "clusters.csv") + np.save(out / "coloc_log2.npy", result.colocalisation) + (out / "settings.json").write_text(json.dumps(result.settings, indent=1, default=str)) + + T = result.table + base = float(result.y_cell.mean()) if result.y_cell is not None else float("nan") + n_pos = sum( + 1 + for r in result.cells.regions + if result.y_cell is not None and result.y_cell[result.cells.region_ids == r][0] == 1 + ) + md = [ + f"# {title}", + "", + f"{len(result.cells.regions)} regions / {result.labels.size} cells; space = {result.settings['space']}; " + f"clustering {json.dumps(result.model.params['clustering'])} on {json.dumps(result.model.params['reduction'])} " + f"(sample {result.model.params['sample_size']}, L2 {result.model.params['l2_normalise']}, {result.model.params['assign_k']}-NN assignment).", + f"Base rate (cells from {positive_name} regions) {base:.3f}; {positive_name} regions {n_pos}/{len(result.cells.regions)}" + + (f"; held-out AUROC {held_out_auroc:.3f}" if held_out_auroc is not None else "") + + f"; {result.n_clusters} clusters, noise {result.noise_fraction:.1%}.", + "", + f"| Cluster | Share | Regions (pos/neg, >{100 * 0.02:.0f}%) | Composition | Enriched (>1.5x) | Emp. frac. pos | Share AUROC | Margin relattn [95% CI] | Signed share | Sign consist. | Lift |", + "|---|---|---|---|---|---|---|---|---|---|---|", + ] + for _, t in T.sort_values("emp_frac_pos", ascending=False).iterrows(): + md.append( + f"| {t.cluster} | {100 * t.share:.1f}% | {t.cores_present} ({t.pos_cores_present}/{t.neg_cores_present}) | {t.top_types} | {t.top_enriched} | " + f"{t.emp_frac_pos:.2f} | {t.share_auroc:.2f} | {t.relattn:+.2f} [{t.relattn_lo:+.2f}, {t.relattn_hi:+.2f}] | {t.signed_share:+.2f} | {t.sign_consistency:.2f} | {t.lift:.2f} |" + ) + conc = result.concordance + if "spearman_emp_relattn" in conc: + md.append( + f"\nSpearman(emp_frac_pos, relattn) over {conc['n_clusters']} clusters: {conc['spearman_emp_relattn']:+.2f}" + + ( + f"; Spearman(share AUROC, relattn): {conc['spearman_share_auroc_relattn']:+.2f}" + if "spearman_share_auroc_relattn" in conc + else "" + ) + ) + CO, CL = result.colocalisation, result.clusters + n = len(CL) + if n >= 2: + pairs = [(i, j) for i in range(n) for j in range(i + 1, n)] + md.append( + "\nCo-localisation log2(obs/exp) of cell-cell edges, top pairs: " + + "; ".join( + f"C{CL[i]}-C{CL[j]} {CO[i, j]:+.1f}" + for i, j in sorted(pairs, key=lambda p: -CO[p])[:8] + ) + ) + md.append( + "Most avoided pairs: " + + "; ".join( + f"C{CL[i]}-C{CL[j]} {CO[i, j]:+.1f}" + for i, j in sorted(pairs, key=lambda p: CO[p])[:5] + ) + ) + (out / "report.md").write_text("\n".join(md) + "\n") + paths["report"] = str(out / "report.md") + + if figures: + try: + from .figures import ( + clusters_overview, + colocalisation_heatmap, + embedding_triptych, + umap_coordinates, + ) + except Exception: # pragma: no cover - matplotlib missing + return paths + samp = result.model.sample_idx + P = result.model.reduce(result.features[samp]) + y_s = result.y_cell[samp] if result.y_cell is not None else np.zeros(samp.size) + if P.shape[1] >= 2: + ev = getattr(result.model.reducer, "explained_variance_ratio_", None) + axis = ( + (f"PC1 ({100 * ev[0]:.1f}% var)", f"PC2 ({100 * ev[1]:.1f}% var)") + if ev is not None + else ("dim 1", "dim 2") + ) + embedding_triptych( + P[:, :2], + result.labels[samp], + result.margin[samp], + y_s, + out / "pca_triptych.png", + title=f"{result.settings['space']} (PCA)", + axis_labels=axis, + positive_name=positive_name, + ) + paths["pca_triptych"] = str(out / "pca_triptych.png") + ucfg = dict(umap or {}) + if ucfg.get("enabled", True): + U = umap_coordinates( + P, + n_neighbors=int(ucfg.get("n_neighbors", 30)), + min_dist=float(ucfg.get("min_dist", 0.1)), + metric=str(ucfg.get("metric", "cosine")), + seed=int(ucfg.get("seed", 0)), + ) + if U is not None: + embedding_triptych( + U, + result.labels[samp], + result.margin[samp], + y_s, + out / "umap_triptych.png", + title=f"{result.settings['space']} (UMAP)", + axis_labels=("UMAP 1", "UMAP 2"), + positive_name=positive_name, + ) + paths["umap_triptych"] = str(out / "umap_triptych.png") + cohort = np.bincount(result.cells.ct, minlength=len(result.cells.cell_type_names)) / max( + 1, result.cells.ct.size + ) + clusters_overview( + T, + out / "clusters_overview.png", + cell_type_names=result.cells.cell_type_names, + cohort_frequency=cohort, + base_rate=base, + positive_name=positive_name, + ) + paths["clusters_overview"] = str(out / "clusters_overview.png") + colocalisation_heatmap(CO, CL, out / "colocalisation.png") + paths["colocalisation"] = str(out / "colocalisation.png") + return paths diff --git a/src/grass_mil/interpretability/niches/tables.py b/src/grass_mil/interpretability/niches/tables.py new file mode 100644 index 0000000..6326508 --- /dev/null +++ b/src/grass_mil/interpretability/niches/tables.py @@ -0,0 +1,131 @@ +"""Per-niche summary table (the preprint's cluster table).""" + +from __future__ import annotations + +from typing import Dict, List, Optional, Sequence + +import numpy as np +import pandas as pd + +__all__ = ["niche_table", "spearman_concordance", "bootstrap_mean"] + + +def bootstrap_mean(values, *, n_boot: int = 2000, seed: int = 0): + v = np.asarray(values, dtype=float) + v = v[np.isfinite(v)] + if v.size == 0: + return float("nan"), float("nan"), float("nan") + g = np.random.default_rng(seed) + bs = [g.choice(v, v.size, replace=True).mean() for _ in range(int(n_boot))] + return float(v.mean()), float(np.percentile(bs, 2.5)), float(np.percentile(bs, 97.5)) + + +def niche_table( + labels: np.ndarray, + *, + cell_types: np.ndarray, + region_ids: np.ndarray, + region_labels: Dict[str, int], + margin: np.ndarray, + attention_pos: np.ndarray, + cell_type_names: Optional[Sequence[str]] = None, + present_threshold: float = 0.02, + enrichment_threshold: float = 1.5, + n_boot: int = 2000, + seed: int = 0, + include_noise: bool = True, +) -> pd.DataFrame: + """One row per niche: share, cores present, composition, enrichment, empirical + fraction of cells from positive regions, share-AUROC, relattn margin with a + bootstrap CI over regions, signed share, sign consistency and attention lift. + """ + from sklearn.metrics import roc_auc_score + + labels = np.asarray(labels).astype(int) + ct = np.asarray(cell_types).astype(int) + rid = np.asarray(region_ids) + M = np.asarray(margin, dtype=float) + A1 = np.asarray(attention_pos, dtype=float) + K = int(ct.max()) + 1 if ct.size else 0 + if cell_type_names is not None: + K = max(K, len(cell_type_names)) + names = list(cell_type_names) if cell_type_names is not None else [str(i) for i in range(K)] + regions = sorted(set(rid.tolist())) + y_region = np.array([int(region_labels[r]) for r in regions]) + y_cell = np.array([int(region_labels[r]) for r in rid]) + cohort = np.bincount(ct, minlength=K) / max(1, ct.size) + region_mask = {r: rid == r for r in regions} + region_size = {r: int(m.sum()) for r, m in region_mask.items()} + region_margin = {r: float(M[m].sum()) for r, m in region_mask.items()} + clusters = sorted(set(labels.tolist()) - {-1}) + if include_noise and (labels == -1).any(): + clusters = clusters + [-1] + rows: List[dict] = [] + for cl in clusters: + msk = labels == cl + comp = np.bincount(ct[msk], minlength=K) / max(1, msk.sum()) + enr = comp / np.maximum(cohort, 1e-9) + vals, shares, signs, lifts = [], [], [], [] + per_region_share = np.zeros(len(regions)) + for j, r in enumerate(regions): + mm = msk & region_mask[r] + s = mm.sum() / max(1, region_size[r]) + per_region_share[j] = s + if mm.any(): + vals.append(M[mm].mean()) + mcr = float(M[mm].sum()) + Mr = region_margin[r] + if abs(Mr) > 1e-6: + shares.append(mcr / Mr) + signs.append(float(np.sign(mcr) == np.sign(Mr))) + lifts.append(A1[mm].sum() / s) + present = per_region_share > present_threshold + m_, lo, hi = bootstrap_mean(vals, n_boot=n_boot, seed=seed) + ss = bootstrap_mean(shares, n_boot=n_boot, seed=seed)[0] if shares else float("nan") + both = 0 < y_region.sum() < y_region.size + rows.append( + dict( + cluster="Noise" if cl == -1 else f"C{cl}", + cluster_id=int(cl), + share=float(msk.mean()), + n_cells=int(msk.sum()), + cores_present=int(present.sum()), + pos_cores_present=int((present & (y_region == 1)).sum()), + neg_cores_present=int((present & (y_region == 0)).sum()), + emp_frac_pos=float(y_cell[msk].mean()) if msk.any() else float("nan"), + share_auroc=float(roc_auc_score(y_region, per_region_share)) + if both + else float("nan"), + relattn=m_, + relattn_lo=lo, + relattn_hi=hi, + signed_share=ss, + sign_consistency=float(np.mean(signs)) if signs else float("nan"), + lift=float(np.mean(lifts)) if lifts else float("nan"), + top_types=", ".join( + f"{names[j]} {100 * comp[j]:.0f}%" for j in np.argsort(-comp)[:4] + ), + top_enriched=", ".join( + f"{names[j]} x{enr[j]:.1f}" + for j in np.argsort(-enr)[:4] + if enr[j] > enrichment_threshold + ), + **{f"comp_{names[j]}": float(comp[j]) for j in range(K)}, + ) + ) + return pd.DataFrame(rows) + + +def spearman_concordance(table: pd.DataFrame) -> Dict[str, float]: + """Spearman correlations between the empirical association and the margin.""" + from scipy.stats import spearmanr + + tt = table[table["cluster"] != "Noise"] + out = {"n_clusters": int(len(tt))} + if len(tt) >= 3: + out["spearman_emp_relattn"] = float(spearmanr(tt["emp_frac_pos"], tt["relattn"])[0]) + if tt["share_auroc"].notna().sum() >= 3: + out["spearman_share_auroc_relattn"] = float( + spearmanr(tt["share_auroc"], tt["relattn"], nan_policy="omit")[0] + ) + return out diff --git a/src/grass_mil/interpretability/niches_cli.py b/src/grass_mil/interpretability/niches_cli.py new file mode 100644 index 0000000..09dc887 --- /dev/null +++ b/src/grass_mil/interpretability/niches_cli.py @@ -0,0 +1,105 @@ +"""``grass-mil-niches``: niche discovery on one model's cell cache.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Dict, Optional + +import hydra +import pandas as pd +from omegaconf import DictConfig, OmegaConf + +from grass_mil._paths import setup_project_root + +setup_project_root() + +from grass_mil.inference.cell_cache import CellCache # noqa: E402 +from grass_mil.interpretability.niches.pipeline import run_niche_discovery, write_niche_outputs # noqa: E402 +from grass_mil.utils import RankedLogger # noqa: E402 + +log = RankedLogger(__name__, rank_zero_only=True) + +__all__ = ["run_niches", "main", "resolve_regions", "load_region_labels"] + + +def resolve_regions(value, cache: CellCache): + if value is None or (isinstance(value, str) and value.lower() == "held_out"): + return cache.held_out or cache.regions + if isinstance(value, str) and value.lower() == "all": + return cache.regions + if isinstance(value, str): + p = Path(value) + if p.suffix.lower() == ".json": + return [str(v) for v in json.loads(p.read_text())] + return [line.strip() for line in p.read_text().splitlines() if line.strip()] + return [str(v) for v in value] + + +def load_region_labels(cfg_labels) -> Optional[Dict[str, int]]: + if not cfg_labels or not cfg_labels.get("label_file"): + return None + df = pd.read_csv(str(cfg_labels.label_file)) + df.columns = [str(c).strip() for c in df.columns] + idc = str(cfg_labels.get("id_column", "REGION_ID")) + lc = cfg_labels.get("label_column") + if not lc: + raise ValueError("labels.label_column is required with labels.label_file") + df = df.dropna(subset=[str(lc)]) + return {str(r): int(float(v) >= 0.5) for r, v in zip(df[idc], df[str(lc)])} + + +def run_niches(cfg: DictConfig) -> Dict[str, Any]: + cache = CellCache.open(str(cfg.cells_dir)) + regions = resolve_regions(cfg.get("regions"), cache) + clustering = OmegaConf.to_container(cfg.clustering, resolve=True) + reduction = OmegaConf.to_container(cfg.reduction, resolve=True) + result = run_niche_discovery( + cache, + regions=regions, + labels_of=load_region_labels(cfg.get("labels")), + space=str(cfg.get("space", "gnn")), + attention_ckpt=cfg.get("attention_ckpt") or cache.index.get("ckpt_path"), + reduction=reduction, + clustering=clustering, + sample_size=cfg.get("sample_size"), + l2_normalise=bool(cfg.get("l2_normalise", True)), + assign_k=int(cfg.get("assign_k", 15)), + seed=int(cfg.get("seed", 0)), + positive_class=int(cfg.get("positive_class", 1)), + present_threshold=float(cfg.get("present_threshold", 0.02)), + n_boot=int(cfg.get("n_boot", 2000)), + ) + title = ( + cfg.get("title") + or f"Niche discovery: {cache.index.get('model_name', Path(str(cfg.cells_dir)).name)}" + ) + paths = write_niche_outputs( + result, + str(cfg.output_dir), + title=str(title), + positive_name=str(cfg.get("positive_name", "positive")), + figures=bool(cfg.get("figures", True)), + umap=OmegaConf.to_container(cfg.umap, resolve=True) + if cfg.get("umap") is not None + else None, + held_out_auroc=cfg.get("held_out_auroc"), + ) + log.info( + f"{result.n_clusters} clusters, noise {result.noise_fraction:.1%}; report: {paths['report']}" + ) + return { + "n_clusters": result.n_clusters, + "noise_fraction": result.noise_fraction, + "concordance": result.concordance, + "paths": paths, + } + + +@hydra.main(version_base="1.3", config_path="../configs", config_name="niches.yaml") +def main(cfg: DictConfig) -> None: + run_niches(cfg) + + +if __name__ == "__main__": + main() diff --git a/src/grass_mil/interpretability/pipeline.py b/src/grass_mil/interpretability/pipeline.py index 881f882..2fb1cde 100644 --- a/src/grass_mil/interpretability/pipeline.py +++ b/src/grass_mil/interpretability/pipeline.py @@ -21,6 +21,7 @@ ) from grass_mil.interpretability.core.biomarkers import niche_composition_summary from grass_mil.interpretability.core.niches import relabel_niches_by_composition +from grass_mil.contracts import ClusteringResult from grass_mil.interpretability.core.clustering import run_clustering from grass_mil.interpretability.core.data import extract_embedding_set from grass_mil.interpretability.core.reduction import run_reduction @@ -178,8 +179,39 @@ def run_interpretability_pipeline( params = _resolve_adaptive_min_cluster_size( params, n_instances=int(clustering_input.shape[0]), method=method ) - clustering_result = run_clustering(method, clustering_input, **params) - labels = clustering_result.labels + sample_size = clustering_cfg.get("sample_size") + l2_normalise = bool(clustering_cfg.get("l2_normalise", False)) + if sample_size is not None or l2_normalise: + # Preprint recipe: cluster a sample on L2-normalised coordinates and + # assign the remaining instances by k-NN (see core.clustering). + from grass_mil.interpretability.core.clustering import fit_niche_clustering + + cl_params = { + k: v for k, v in params.items() if k not in {"n_clusters", "random_state"} + } + niche_model, labels = fit_niche_clustering( + clustering_input, + reduction={"method": "none"}, + clustering={ + "method": method, + "params": cl_params, + "n_clusters": params.get("n_clusters"), + "random_state": params.get("random_state", 42), + }, + sample_size=int(sample_size) if sample_size is not None else None, + l2_normalise=l2_normalise, + assign_k=int(clustering_cfg.get("assign_k", 15)), + seed=int(clustering_cfg.get("seed", 0)), + ) + clustering_result = ClusteringResult( + method=str(method).lower(), + labels=np.asarray(labels), + params={**params, **niche_model.params}, + fitted_object=niche_model, + ) + else: + clustering_result = run_clustering(method, clustering_input, **params) + labels = clustering_result.labels niche_relabel_map: Dict[Any, Any] = {} if labels is not None and bool(config.get("relabel_niches_by_composition", True)): diff --git a/src/grass_mil/interpretability/plugins/builtin.py b/src/grass_mil/interpretability/plugins/builtin.py index 090405e..337cf18 100644 --- a/src/grass_mil/interpretability/plugins/builtin.py +++ b/src/grass_mil/interpretability/plugins/builtin.py @@ -203,6 +203,30 @@ def _resolve_logit_bias(params: Dict[str, Any]) -> Optional[List[float]]: return bias +def _bag_logits_from_table(bag_table, *, bag_id_column: str, logit_prefix: str): + """``{bag_id: [logit_0, logit_1, ...]}`` from a predictions table, or None. + + With the bag logits available the attribution reports a real + ``identity_residual`` (max |sum_i A[i,c] l[i,c] - L_c| over bags) instead + of NaN, so the additive identity is checked rather than assumed. + """ + if bag_table is None or bag_id_column not in bag_table.columns: + return None + import re + + pattern = re.compile(rf"^{re.escape(logit_prefix)}(\d+)$") + cols = sorted( + (c for c in bag_table.columns if pattern.match(str(c))), + key=lambda c: int(pattern.match(str(c)).group(1)), + ) + if not cols: + return None + return { + str(row[bag_id_column]): [float(row[c]) for c in cols] + for _, row in bag_table[[bag_id_column, *cols]].iterrows() + } + + @dataclass class MarginAttributionPlugin(InterpretabilityPlugin): """Exact additive attribution of bag decisions to instance niches. @@ -236,16 +260,22 @@ def run(self, dataset, context: PluginContext, **params: Any) -> PluginResult: "Enable the interpretability export in the predict step." ) + bag_id_column = str(params.get("bag_id_column", dataset.bag_id_column)) result = niche_attribution_summary( table, labels, attention_columns=attention_columns, logit_columns=logit_columns, - bag_id_column=str(params.get("bag_id_column", dataset.bag_id_column)), + bag_id_column=bag_id_column, logit_bias=_resolve_logit_bias(params), n_bootstrap=int(params.get("n_bootstrap", 200)), random_state=int(params.get("random_state", 0)), margin_eps=float(params.get("margin_eps", 1e-6)), + bag_logits=_bag_logits_from_table( + getattr(dataset, "bag_table", None), + bag_id_column=str(params.get("bag_table_id_column", "bag_id")), + logit_prefix=str(params.get("bag_logit_prefix", "logit_")), + ), focus_classes=params.get("focus_classes"), ) payload = { diff --git a/src/grass_mil/interpretability/tier2/autocorrelation.py b/src/grass_mil/interpretability/tier2/autocorrelation.py index 0f1914b..87254c1 100644 --- a/src/grass_mil/interpretability/tier2/autocorrelation.py +++ b/src/grass_mil/interpretability/tier2/autocorrelation.py @@ -21,12 +21,12 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Dict, List, Sequence +from typing import Dict, Sequence import numpy as np import pandas as pd -__all__ = ["MoranResult", "morans_i", "run_morans_i", "diff_morans_i_vs_reference"] +__all__ = ["MoranResult", "morans_i", "run_morans_i"] @dataclass(frozen=True) @@ -211,35 +211,3 @@ def run_morans_i( qvalue=q_frame, n_nodes=pd.Series(counts).reindex(stat_frame.index), ) - - -def diff_morans_i_vs_reference( - result: MoranResult, - *, - reference_group: object = -1, -) -> pd.DataFrame: - """Differential autocorrelation against a reference group. - - ``z = (I_k - I_ref) / sqrt(var_k + var_ref)``, using the permutation-null - variances. The reference is typically the Background niche. - """ - if reference_group not in result.statistic.index: - raise ValueError( - f"reference_group {reference_group!r} is not present; available: " - f"{list(result.statistic.index)}." - ) - ref_stat = result.statistic.loc[reference_group] - ref_var = result.variance.loc[reference_group] - - rows: List[pd.Series] = [] - index: List[object] = [] - for group in result.statistic.index: - if group == reference_group: - continue - delta = result.statistic.loc[group] - ref_stat - pooled = np.sqrt(result.variance.loc[group].astype(float) + ref_var.astype(float)) - rows.append(delta / pooled.replace(0.0, np.nan)) - index.append(group) - if not rows: - return pd.DataFrame(columns=result.statistic.columns) - return pd.DataFrame(rows, index=index) diff --git a/src/grass_mil/models/bgrl_module.py b/src/grass_mil/models/bgrl_module.py index a2f1d51..1ab2931 100644 --- a/src/grass_mil/models/bgrl_module.py +++ b/src/grass_mil/models/bgrl_module.py @@ -29,7 +29,23 @@ def _molecular_feature_names(module) -> Optional[list]: if dataset is None or len(dataset) == 0: return None names = getattr(dataset[0], "molecular_feature_names", None) - return list(names) if names else None + if names: + return list(names) + # Older precomputes record the names only in metadata.json next to the index. + index_path = getattr(getattr(dataset, "dataset", dataset), "index_path", None) + if index_path is not None: + import json + from pathlib import Path + + meta = Path(index_path).parent / "metadata.json" + if meta.exists(): + try: + names = json.loads(meta.read_text()).get("molecular_feature_names") + except (OSError, ValueError): + names = None + if names: + return list(map(str, names)) + return None class BGRLModule(L.LightningModule): @@ -72,12 +88,23 @@ def __init__( ) self._augmentation_seed = int(self.task_cfg.get("augmentation_seed", 0)) self._view_generator: Optional[torch.Generator] = None + # EMA momentum of the target encoder: cosine from `momentum` to + # `momentum_final` (preprint: 0.99 -> 1.0, no warmup). `momentum_min` is + # the legacy name of the final value. self._momentum_base = float(self.task_cfg.get("momentum", 0.99)) - self._momentum_min = float(self.task_cfg.get("momentum_min", 0.99)) + final = self.task_cfg.get("momentum_final") + if final is None: + final = self.task_cfg.get("momentum_min", self._momentum_base) + self._momentum_min = float(final) self._warmup_steps = int(self.task_cfg.get("warmup_steps", 0)) + momentum_warmup = self.task_cfg.get("momentum_warmup_steps") + self._momentum_warmup_steps = ( + self._warmup_steps if momentum_warmup is None else int(momentum_warmup) + ) self._total_steps = int(self.task_cfg.get("total_steps", 0)) self._momentum_scheduler: Optional[CosineWarmup] = None + self._last_momentum_step = 0 # When set, replaces the uniform drop_edge/drop_feat views. self._augmentation_cfg = self.task_cfg.get("augmentation") self._augmentation = None @@ -125,7 +152,7 @@ def setup(self, stage: Optional[str] = None) -> None: self.log("init/total_steps_warning", float(total_steps), prog_bar=False) self._momentum_scheduler = CosineWarmup( base_value=self._momentum_base, - warmup_steps=max(0, self._warmup_steps), + warmup_steps=max(0, self._momentum_warmup_steps), total_steps=max(1, total_steps), min_value=self._momentum_min, ) @@ -186,12 +213,20 @@ def test_step(self, batch, batch_idx): self.log("test/loss", loss, on_step=False, on_epoch=True, prog_bar=True) return loss - def on_before_zero_grad(self, optimizer) -> None: + def on_train_batch_end(self, outputs, batch, batch_idx) -> None: + """EMA update of the target encoder after every optimizer step. + + Runs once per completed optimizer step (``global_step`` has just been + incremented), with the momentum of that zero-based step, exactly like + the reference loop (``update_target_network(mm_now)`` after ``step()``). + """ if self.ssl_model is None or self._momentum_scheduler is None: return - # Lightning increments global_step after optimizer.step(); map back to zero-based step. - step = max(int(self.global_step) - 1, 0) - momentum = self._momentum_scheduler.value(step) + completed = int(self.global_step) + if completed <= self._last_momentum_step: + return # gradient-accumulation sub-batch: no optimizer step happened + self._last_momentum_step = completed + momentum = self._momentum_scheduler.value(completed - 1) self.ssl_model.update_target_network(momentum) self.log("train/momentum", momentum, on_step=True, on_epoch=False, prog_bar=False) @@ -204,10 +239,20 @@ def configure_optimizers(self): "(for example '+optim=adamw') or set model.optim explicitly." ) optimizer = instantiate_optimizer(self.optim_cfg, self.ssl_model.trainable_parameters()) + # `scheduler.T_max: auto` = cosine over the steps after the LR warmup, so + # the learning rate reaches eta_min exactly at `task.total_steps`. + warm_cfg = self.task_cfg.get("lr_warmup") or {} + lr_warmup_steps = ( + int(warm_cfg.get("warmup_steps", 0)) if bool(warm_cfg.get("enabled", False)) else 0 + ) + t_max_auto = None + if self._total_steps > 0: + t_max_auto = max(1, self._total_steps - max(0, lr_warmup_steps)) scheduler = instantiate_scheduler_with_warmup( self.scheduler_cfg, optimizer, warmup_cfg=self.task_cfg.get("lr_warmup"), + t_max_auto=t_max_auto, ) if scheduler is None: return optimizer diff --git a/src/grass_mil/models/components/attention.py b/src/grass_mil/models/components/attention.py index 8266146..c064271 100644 --- a/src/grass_mil/models/components/attention.py +++ b/src/grass_mil/models/components/attention.py @@ -1,10 +1,35 @@ from __future__ import annotations +from typing import Literal + +import torch from torch import nn +InitType = Literal["default", "kaiming"] + + +def _init_linears(module: nn.Module, init: InitType) -> None: + """Apply the requested initialisation to every ``nn.Linear`` in ``module``. + + ``kaiming`` is the reference GRASS-MIL initialisation (``kaiming_uniform_`` on + the weights, biases left at the torch default); ``default`` keeps torch's own. + """ + if init == "default": + return + if init != "kaiming": + raise ValueError(f"init must be 'default' or 'kaiming', got {init!r}.") + for layer in module.modules(): + if isinstance(layer, nn.Linear): + nn.init.kaiming_uniform_(layer.weight) + class AttnNetGated(nn.Module): - """Gated MIL attention network.""" + """Gated MIL attention network (Ilse et al. 2018), one attention channel per class. + + ``forward`` returns the raw (pre-softmax) attention logits of shape + ``[num_instances, n_classes]``; the softmax over instances is applied by the + bag aggregation so that it can be taken within each bag. + """ def __init__( self, @@ -12,6 +37,7 @@ def __init__( hidden_dim: int = 64, dropout: bool = False, n_classes: int = 1, + init: InitType = "kaiming", ) -> None: super().__init__() a_layers = [nn.Linear(input_dim, hidden_dim), nn.Tanh()] @@ -22,6 +48,8 @@ def __init__( self.attention_a = nn.Sequential(*a_layers) self.attention_b = nn.Sequential(*b_layers) self.attention_c = nn.Linear(hidden_dim, n_classes) + self.init = init + _init_linears(self, init) def forward(self, x): a = self.attention_a(x) @@ -29,30 +57,38 @@ def forward(self, x): logits = self.attention_c(a * b) return logits, x + def attention_space(self, x: torch.Tensor) -> torch.Tensor: + """The ``hidden_dim``-d pre-tanh activation, the attention head's own feature space.""" + return self.attention_a[0](x) + def head_spaces(self, x): """Intermediate activations of the attention head. ``a_pre_tanh`` is the argument of the tanh gate, before saturation. It - is the primary clustering space for the interpretability suite because - it is low-dimensional and directly upstream of the attention weights; + is a low-dimensional space directly upstream of the attention weights; clustering the saturated output collapses the extremes together. """ - pre_tanh = self.attention_a[0](x) + pre_tanh = self.attention_space(x) a = self.attention_a(x) b = self.attention_b(x) return {"a_pre_tanh": pre_tanh, "gate_product": a * b} class AttnNetGatedProjected(nn.Module): - """Projected variant of gated MIL attention network.""" + """Projected gated MIL attention: ``Linear + LeakyReLU`` projection, then the gate. + + GRASS-MIL default: ``projection_dim=32``, ``hidden_dim=8``, dropout 0.25 in + training, one channel per class, kaiming-initialised linears. + """ def __init__( self, input_dim: int, - projection_dim: int = 64, - hidden_dim: int = 16, - dropout: bool = False, + projection_dim: int = 32, + hidden_dim: int = 8, + dropout: bool = True, n_classes: int = 1, + init: InitType = "kaiming", ) -> None: super().__init__() projection = [nn.Linear(input_dim, projection_dim), nn.LeakyReLU()] @@ -66,18 +102,28 @@ def __init__( self.attention_a = nn.Sequential(*attn_a) self.attention_b = nn.Sequential(*attn_b) self.attention_c = nn.Linear(hidden_dim, n_classes) + self.init = init + _init_linears(self, init) def forward(self, x): h = self.projection(x) logits = self.attention_c(self.attention_a(h) * self.attention_b(h)) return logits, x + def attention_space(self, x: torch.Tensor) -> torch.Tensor: + """The ``hidden_dim``-d pre-tanh activation ``V · LeakyReLU(W_proj x)``. + + Computed without dropout (``projection[0]`` and ``attention_a[0]`` only), so + it is deterministic in eval and train mode alike. + """ + h = self.projection[1](self.projection[0](x)) + return self.attention_a[0](h) + def head_spaces(self, x): """Intermediate activations of the attention head. ``a_pre_tanh`` is ``V * LeakyReLU(W_proj x)``, the argument of the tanh - gate. It is the primary clustering space for the interpretability - suite: low-dimensional, directly upstream of the attention weights, and + gate: low-dimensional, directly upstream of the attention weights, and not yet collapsed by tanh saturation. """ h = self.projection(x) diff --git a/src/grass_mil/models/components/backbones.py b/src/grass_mil/models/components/backbones.py index 05178b1..a49e9f3 100644 --- a/src/grass_mil/models/components/backbones.py +++ b/src/grass_mil/models/components/backbones.py @@ -44,6 +44,11 @@ class EncoderConfig: # `edge_attr_dim` is 1. Leave unset to pass the whole edge_attr vector. edge_feature_index: Optional[int] = None categorical_embedding: Optional[Dict[str, Any]] = None + # GIN/GINE epsilon: learnable (the GRASS-MIL preprint trains it) or fixed at 0. + train_eps: bool = True + # Initialisation of the input projection (embedding table + linear): "default" + # keeps the torch defaults, "xavier" applies xavier_uniform_ to both (preprint). + input_init: Literal["default", "xavier"] = "xavier" def _get_activation(name: str) -> nn.Module: @@ -82,6 +87,7 @@ def __init__(self, cfg: EncoderConfig): input_dim=cfg.input_dim, hidden_dim=cfg.hidden_dim, categorical=CategoricalEmbeddingConfig.from_dict(cfg.categorical_embedding), + init=cfg.input_init, ) self.layers = nn.ModuleList([self._build_conv_layer() for _ in range(cfg.num_layers)]) @@ -113,7 +119,7 @@ def _build_conv_layer(self) -> nn.Module: nn.ReLU(), nn.Linear(hdim * 2, hdim), ) - return GINConv(mlp) + return GINConv(mlp, train_eps=self.cfg.train_eps) if ctype == "gcn": return GCNConv(hdim, hdim, add_self_loops=True, normalize=True) if ctype == "gat": @@ -138,7 +144,7 @@ def _build_conv_layer(self) -> nn.Module: nn.ReLU(), nn.Linear(hdim * 2, hdim), ) - return GINEConv(mlp, edge_dim=self.cfg.edge_attr_dim) + return GINEConv(mlp, edge_dim=self.cfg.edge_attr_dim, train_eps=self.cfg.train_eps) raise ValueError(f"Unsupported conv_type: {ctype}") def _supports_edge_attr(self) -> bool: diff --git a/src/grass_mil/models/components/embeddings.py b/src/grass_mil/models/components/embeddings.py index 82bc6ed..8408435 100644 --- a/src/grass_mil/models/components/embeddings.py +++ b/src/grass_mil/models/components/embeddings.py @@ -38,18 +38,34 @@ class CategoricalEmbeddingConfig: reserve_unassigned: Reserve one extra row so augmentations can mark a dropped node as "unassigned" without resizing the table. The reserved code is the last row. + unassigned_label: Alternatively, name an EXISTING category (for example + ``"other"``) as the unassigned token; augmentations then write that + category's code instead of a reserved row. Resolved to + ``unassigned_index`` from the datamodule label map. Mutually + exclusive with ``reserve_unassigned``. + unassigned_index: Explicit code of the unassigned token (set directly, + or resolved from ``unassigned_label``). """ label: str = "cell_type" num_embeddings: Optional[int] = None column_index: Optional[int] = None reserve_unassigned: bool = True + unassigned_label: Optional[str] = None + unassigned_index: Optional[int] = None @classmethod def from_dict(cls, cfg: Optional[Mapping[str, Any]]) -> Optional["CategoricalEmbeddingConfig"]: if cfg is None: return None - allowed = {"label", "num_embeddings", "column_index", "reserve_unassigned"} + allowed = { + "label", + "num_embeddings", + "column_index", + "reserve_unassigned", + "unassigned_label", + "unassigned_index", + } unknown = set(cfg) - allowed if unknown: raise ValueError( @@ -72,13 +88,17 @@ def __init__( input_dim: int, hidden_dim: int, categorical: Optional[CategoricalEmbeddingConfig] = None, + init: str = "default", ) -> None: super().__init__() if input_dim < 0: raise ValueError(f"input_dim must be >= 0, got {input_dim}.") + if init not in {"default", "xavier"}: + raise ValueError(f"init must be 'default' or 'xavier', got {init!r}.") self.input_dim = int(input_dim) self.hidden_dim = int(hidden_dim) self.categorical = categorical + self.init = init if self.input_dim == 0: # Cell-type-only cohorts: precompute emits an empty feature matrix @@ -111,6 +131,15 @@ def __init__( raise ValueError( f"categorical.num_embeddings must be >= 1, got {categorical.num_embeddings}." ) + if categorical.reserve_unassigned and ( + categorical.unassigned_label is not None + or categorical.unassigned_index is not None + ): + raise ValueError( + "categorical.reserve_unassigned=True cannot be combined with " + "unassigned_label/unassigned_index: either reserve an extra row or name an " + "existing category as the unassigned token." + ) num_rows = int(categorical.num_embeddings) + int(categorical.reserve_unassigned) self.embedding = nn.Embedding(num_rows, self.hidden_dim) self._column_index = ( @@ -122,6 +151,30 @@ def __init__( ) if categorical.reserve_unassigned: self._unassigned_index = num_rows - 1 + elif categorical.unassigned_index is not None: + idx = int(categorical.unassigned_index) + if idx < 0 or idx >= num_rows: + raise ValueError( + f"categorical.unassigned_index={idx} is out of range for an embedding " + f"with {num_rows} rows." + ) + self._unassigned_index = idx + elif categorical.unassigned_label is not None: + raise ValueError( + f"categorical.unassigned_label={categorical.unassigned_label!r} was not " + "resolved to a code; use resolve_categorical_embedding_config with the " + "datamodule label map, or set unassigned_index." + ) + self.reset_parameters() + + def reset_parameters(self) -> None: + if self.init == "xavier": + if self.embedding is not None: + nn.init.xavier_uniform_(self.embedding.weight) + if isinstance(self.linear, nn.Linear): + # Weight only, as in the reference implementation; the bias keeps + # the torch default. + nn.init.xavier_uniform_(self.linear.weight) @property def unassigned_index(self) -> Optional[int]: @@ -182,11 +235,13 @@ def resolve_categorical_embedding_config( *, cardinalities: Optional[Dict[str, int]] = None, slices: Optional[Mapping[str, int]] = None, + label_maps: Optional[Mapping[str, Mapping[str, int]]] = None, ) -> Optional[CategoricalEmbeddingConfig]: - """Fill in vocabulary size and column index from datamodule metadata. + """Fill in vocabulary size, column index and unassigned code from datamodule metadata. Explicit config values always win; ``None`` triggers inference, mirroring - the ``encoder.input_dim`` contract. + the ``encoder.input_dim`` contract. ``label_maps`` (``{label: {category: code}}``) + is used to turn ``unassigned_label`` into ``unassigned_index``. """ resolved = CategoricalEmbeddingConfig.from_dict(cfg) if resolved is None: @@ -205,9 +260,21 @@ def resolve_categorical_embedding_config( f"'{resolved.label}'. Set model.encoder.categorical_embedding.num_embeddings " "explicitly, or include the label in data.categorical_features.include_labels." ) + unassigned_index = resolved.unassigned_index + if unassigned_index is None and resolved.unassigned_label is not None: + mapping = (label_maps or {}).get(resolved.label) + if not mapping or resolved.unassigned_label not in mapping: + raise ValueError( + f"categorical.unassigned_label={resolved.unassigned_label!r} is not a category " + f"of '{resolved.label}'. Known categories: " + f"{sorted(mapping) if mapping else 'none (no label map available)'}." + ) + unassigned_index = int(mapping[resolved.unassigned_label]) return CategoricalEmbeddingConfig( label=resolved.label, num_embeddings=int(num_embeddings), column_index=None if column_index is None else int(column_index), reserve_unassigned=resolved.reserve_unassigned, + unassigned_label=resolved.unassigned_label, + unassigned_index=unassigned_index, ) diff --git a/src/grass_mil/models/components/factory.py b/src/grass_mil/models/components/factory.py index 21085d7..557aab7 100644 --- a/src/grass_mil/models/components/factory.py +++ b/src/grass_mil/models/components/factory.py @@ -2,6 +2,8 @@ from typing import Any, Dict, Optional +import torch + from .attention import AttnNetGated, AttnNetGatedProjected from .backbones import EncoderConfig, GNNEncoder from .heads import GraphPredictionHead @@ -67,8 +69,29 @@ def build_ssl(use_ssl: bool, encoder, ssl_config: Optional[Dict[str, Any]] = Non return BGRL(encoder=encoder, predictor=predictor) +def _as_weight_tensor(value): + """Accept tensors, lists/tuples and OmegaConf sequences for loss weights.""" + if value is None or isinstance(value, torch.Tensor): + return value + try: + from omegaconf import ListConfig, OmegaConf + + if isinstance(value, ListConfig): + value = OmegaConf.to_container(value, resolve=True) + except ImportError: # pragma: no cover + pass + if isinstance(value, (list, tuple)): + return torch.as_tensor([float(v) for v in value], dtype=torch.float32) + if isinstance(value, (int, float)): + return torch.as_tensor([float(value)], dtype=torch.float32) + raise TypeError(f"Loss weight must be a tensor or a sequence of floats, got {type(value)!r}.") + + def build_loss(loss_type: str, **kwargs): name = loss_type.lower() + for key in ("class_weight", "pos_weight"): + if key in kwargs: + kwargs[key] = _as_weight_tensor(kwargs[key]) if name == "categorical_ce": return WeightedCrossEntropyLoss(**kwargs) if name == "categorical_bce": diff --git a/src/grass_mil/models/supervised_module.py b/src/grass_mil/models/supervised_module.py index bb578a8..d3b9eca 100644 --- a/src/grass_mil/models/supervised_module.py +++ b/src/grass_mil/models/supervised_module.py @@ -5,6 +5,8 @@ import lightning as L import torch import torch.distributed as dist +from omegaconf import DictConfig, OmegaConf + from grass_mil.batching import extract_subgraphs from grass_mil.inference.aggregation import aggregate_group_logits from grass_mil.inference.metrics import compute_task_metrics_from_tensors @@ -13,7 +15,10 @@ from .training import ( aggregate_bag_logits_attention, aggregate_bag_logits_mean, + build_finetune_augmentation, + build_param_groups, build_supervised_components, + collect_unit_labels, compute_binary_accuracy, compute_categorical_accuracy, compute_supervised_loss, @@ -22,7 +27,9 @@ group_instance_indices_by_bag, instantiate_optimizer, instantiate_scheduler_with_warmup, + inverse_frequency_class_weights, load_state_dict_with_optional_mapping, + resolve_t_max_auto, validate_task_config, ) @@ -101,6 +108,23 @@ def __init__( self.region_accum_hyperbatch_size = 0 self.region_accum_flush_on_epoch_end = False + # Regime knobs (all optional; see configs/regime/*.yaml for the presets). + self.optimization_cfg = dict(self.task_cfg.get("optimization", {}) or {}) + self.class_weights_cfg = self.task_cfg.get("class_weights") + self.finetune_aug_cfg = self.task_cfg.get("finetune_augmentation") + self.head_init_path = self.task_cfg.get("head_init_path") + self.freeze_head = bool(self.task_cfg.get("freeze_head", False)) + grad_clip = self.task_cfg.get("grad_clip_norm") + self.grad_clip_norm = None if grad_clip in (None, "", "none") else float(grad_clip) + self.scheduler_interval = str(self.task_cfg.get("scheduler_interval", "epoch")) + if self.scheduler_interval not in {"epoch", "step"}: + raise ValueError("task.scheduler_interval must be 'epoch' or 'step'.") + self._finetune_augment = None + self._encoder_frozen = False + self._eval_stage = "val" + self.test_predictions: Optional[Dict[str, Any]] = None + self._skipped_optimizer_steps = 0 + self._region_loss_buffer: list[torch.Tensor] = [] self._manual_optimizer_steps = 0 # In inference, callers can disable instance-level payload emission to reduce @@ -159,16 +183,110 @@ def setup(self, stage: Optional[str] = None) -> None: init_strict=self.init_strict, encoder_init_map=self.encoder_init_map, ) - if self.freeze_encoder: + encoder_scale = float(self.optimization_cfg.get("encoder_lr_scale", 1.0) or 0.0) + if self.freeze_encoder or encoder_scale <= 0.0: + # A zero learning-rate scale is the preprint's "frozen encoder": no + # gradients, no optimizer group, and the encoder stays in eval mode + # (dropout/batchnorm off) throughout training. for p in self.encoder.parameters(): p.requires_grad = False + self._encoder_frozen = True + self.encoder.eval() + + if self.head_init_path: + if str(self.head_init_path).strip().lower() == "auto": + raise ValueError( + "task.head_init_path='auto' is only resolved by grass-mil-nested-cv, which " + "fits a probe head per split. For a single run, fit one with grass-mil-probe " + "and pass task.head_init_path=/path/to/probe_head.pt." + ) + self._load_head_init(str(self.head_init_path)) + if self.freeze_head: + for p in self.graph_head.parameters(): + p.requires_grad = False + + self._configure_class_weights(stage) + self._configure_finetune_augmentation() self._built = True + def _load_head_init(self, path: str) -> None: + """Initialise the graph head from a probe-head file (``graph_head.net.*`` keys).""" + state = torch.load(path, map_location="cpu", weights_only=False) + if isinstance(state, dict) and "state_dict" in state: + state = state["state_dict"] + head_state = { + k.split("graph_head.", 1)[1]: v + for k, v in state.items() + if isinstance(k, str) and k.startswith("graph_head.") and isinstance(v, torch.Tensor) + } + if not head_state: + raise ValueError(f"No 'graph_head.*' tensors found in head_init_path={path!r}.") + missing, unexpected = self.graph_head.load_state_dict(head_state, strict=False) + if unexpected: + raise ValueError(f"head_init_path carries keys the head does not have: {unexpected}") + if missing: + raise ValueError( + f"head_init_path does not cover the whole head (missing {missing}); a probe " + "pre-fit targets a linear head (model.graph_head.num_layers=1)." + ) + + def _configure_class_weights(self, stage: Optional[str]) -> None: + cfg = self.class_weights_cfg + if cfg is None or stage not in (None, "fit"): + return + if not hasattr(self.loss_fn, "class_weight"): + return + if isinstance(cfg, str): + if cfg.lower() != "auto_train": + raise ValueError("task.class_weights must be 'auto_train', a list, or null.") + labels = self._training_unit_labels() + if labels is None: + return + num_classes = int(self.hparams.graph_head.get("output_dim", 1)) + if num_classes <= 1: + return + weights = inverse_frequency_class_weights(labels, num_classes) + else: + weights = torch.as_tensor([float(v) for v in cfg], dtype=torch.float32) + self.loss_fn.class_weight = weights + self._class_weights = weights + + def _training_unit_labels(self) -> Optional[list[int]]: + try: + trainer = self.trainer + except RuntimeError: + return None + if trainer is None or trainer.datamodule is None: + return None + dataset = getattr(trainer.datamodule, "dataset_train", None) + if dataset is None: + return None + labels = collect_unit_labels(dataset, target_columns=self.task_cfg.get("target_columns")) + return labels or None + + def _configure_finetune_augmentation(self) -> None: + if not self.finetune_aug_cfg: + return + input_proj = getattr(self.encoder, "input_proj", None) + self._finetune_augment = build_finetune_augmentation( + self.finetune_aug_cfg, + unassigned_index=getattr(input_proj, "unassigned_index", None), + categorical_column=getattr(input_proj, "_column_index", None), + ) + + def train(self, mode: bool = True): + super().train(mode) + if mode and self._encoder_frozen and self.encoder is not None: + self.encoder.eval() + return self + # ------------------------------------------------------------------ # Forward helpers # ------------------------------------------------------------------ def _forward_instances(self, batch): + if self.training and self._finetune_augment is not None: + batch = self._finetune_augment(batch) node_emb, graph_emb = self.encoder( batch.x, batch.edge_index, @@ -768,7 +886,13 @@ def training_step(self, batch, batch_idx): return mean_region_loss_value def validation_step(self, batch, batch_idx): - out = self._shared_step(batch, "val") + return self._eval_step(batch, "val") + + def _eval_step(self, batch, stage: str): + """Shared val/test step: buffer instance-level outputs for region-level + aggregation at epoch end, so a region split over several sub-batches is + scored exactly once with one attention softmax over all of its cells.""" + out = self._shared_step(batch, stage) metadata = self._build_predict_group_metadata( batch, patch_logits=out["patch_logits"], @@ -783,7 +907,13 @@ def validation_step(self, batch, batch_idx): if out["bag_targets"] is not None: self._val_bag_targets_chunks.append(out["bag_targets"].detach().cpu()) self._val_instance_logits_chunks.append(metadata["instance_logits"].detach().cpu()) - self._val_instance_patch_ids.extend([str(v) for v in metadata["instance_patch_ids"]]) + if str(self.task_cfg.get("bag_key", "region_id")) == "patch_id": + # Patch-level bags: group instances by the bag they were pooled in + # (the tile), not by their ego-graph id, so the epoch-end + # re-aggregation reproduces the training bags exactly. + self._val_instance_patch_ids.extend([str(v) for v in metadata["instance_bag_ids"]]) + else: + self._val_instance_patch_ids.extend([str(v) for v in metadata["instance_patch_ids"]]) self._val_instance_region_ids.extend(metadata["instance_region_ids"]) self._val_instance_sample_ids.extend(metadata["instance_sample_ids"]) if "instance_attention_logits" in metadata: @@ -793,7 +923,15 @@ def validation_step(self, batch, batch_idx): return out def test_step(self, batch, batch_idx): - return self._shared_step(batch, "test") + return self._eval_step(batch, "test") + + def on_test_epoch_start(self) -> None: + self._eval_stage = "test" + self._reset_val_epoch_buffers() + + def on_test_epoch_end(self) -> None: + self._finish_eval_epoch("test") + self._eval_stage = "val" def predict_step(self, batch, batch_idx, dataloader_idx=0): out = self._forward_bags(batch, allow_missing_targets=True) @@ -878,16 +1016,30 @@ def _flush_region_buffer_if_needed(self, *, force: bool) -> list[torch.Tensor]: flush_loss = torch.stack(chunk, dim=0).mean() self.manual_backward(flush_loss) + if self.grad_clip_norm is not None: + self.clip_gradients( + optimizer, + gradient_clip_val=float(self.grad_clip_norm), + gradient_clip_algorithm="norm", + ) + scaler = self._amp_scaler() + scale_before = float(scaler.get_scale()) if scaler is not None else None optimizer.step() optimizer.zero_grad(set_to_none=True) - - scheduler = self.lr_schedulers() - if scheduler is not None: - if isinstance(scheduler, (list, tuple)): - for sch in scheduler: - sch.step() - else: - scheduler.step() + # Under mixed precision a step with inf/nan gradients is skipped and the + # loss scale is reduced; the preprint only advances the schedule on + # steps that actually updated the weights. + stepped = scaler is None or float(scaler.get_scale()) >= scale_before + if stepped: + scheduler = self.lr_schedulers() + if scheduler is not None: + if isinstance(scheduler, (list, tuple)): + for sch in scheduler: + sch.step() + else: + scheduler.step() + else: + self._skipped_optimizer_steps += 1 self._manual_optimizer_steps += 1 self.log( "train/manual_optimizer_steps", @@ -904,6 +1056,14 @@ def _flush_region_buffer_if_needed(self, *, force: bool) -> list[torch.Tensor]: return flush_losses + def _amp_scaler(self): + """The GradScaler of the active mixed-precision plugin, if any.""" + try: + plugin = self.trainer.precision_plugin + except RuntimeError: + return None + return getattr(plugin, "scaler", None) + def on_train_start(self) -> None: if self.region_accum_enabled: self._region_loss_buffer.clear() @@ -912,14 +1072,21 @@ def on_validation_epoch_start(self) -> None: self._reset_val_epoch_buffers() def on_validation_epoch_end(self) -> None: + self._finish_eval_epoch("val") + + def _finish_eval_epoch(self, stage: str) -> None: payload = self._build_validation_epoch_payload() if payload is None or payload.bag_targets is None: self._reset_val_epoch_buffers() return + # Region-level metrics are computed per bag unit: the task's bag key + # decides whether a unit is a region (default), a patch/tile or a sample. + bag_key = str(self.task_cfg.get("bag_key", "region_id")) + bag_scope = {"patch_id": "patch", "sample_id": "sample"}.get(bag_key, "region") aggregated = aggregate_group_logits( payload, mode="attention_weighted" if self.use_attention else "mean", - bag_scope="region", + bag_scope=bag_scope, subsample_fraction=1.0, subsample_seed=None, ) @@ -933,45 +1100,99 @@ def on_validation_epoch_end(self) -> None: targets=aggregated.bag_targets, threshold=0.5, ) - if target_type in {"binary", "categorical"}: + region_metrics = self._region_level_metrics( + target_type, aggregated.bag_logits, aggregated.bag_targets + ) + + def _log(name: str, value: float, prog_bar: bool = False) -> None: self.log( - "val/acc", - torch.tensor(float(metrics["accuracy"]), device=self.device), + f"{stage}/{name}", + torch.tensor(float(value), device=self.device), on_step=False, on_epoch=True, - prog_bar=True, + prog_bar=prog_bar, ) + + if target_type in {"binary", "categorical"}: + _log("acc", metrics["accuracy"], prog_bar=True) + for name, value in region_metrics.items(): + _log(name, value, prog_bar=name == "auroc") elif target_type == "regression": - self.log( - "val/mae", - torch.tensor(float(metrics["mae"]), device=self.device), - on_step=False, - on_epoch=True, - prog_bar=True, - ) - self.log( - "val/rmse", - torch.tensor(float(metrics["rmse"]), device=self.device), - on_step=False, - on_epoch=True, - prog_bar=True, + _log("mae", metrics["mae"], prog_bar=True) + _log("rmse", metrics["rmse"], prog_bar=True) + _log("r2", metrics["r2"]) + elif target_type == "survival": + _log("c_index", metrics["c_index"], prog_bar=True) + + if stage == "test": + self.test_predictions = { + "bag_ids": list(aggregated.bag_ids), + "logits": aggregated.bag_logits.detach().cpu(), + "targets": aggregated.bag_targets.detach().cpu(), + "region_ids": list(getattr(aggregated, "row_region_ids", None) or []), + "sample_ids": list(getattr(aggregated, "row_sample_ids", None) or []), + "target_type": target_type, + } + self._reset_val_epoch_buffers() + + @staticmethod + def _region_level_metrics( + target_type: str, logits: torch.Tensor, targets: torch.Tensor + ) -> Dict[str, float]: + """Unweighted region loss, AUROC and balanced accuracy on aggregated region logits. + + These are the quantities the preprint selects and early-stops on: the loss + of the region-level prediction (not of training sub-batches) and the + AUROC of the positive-class probability. + """ + import numpy as np + from sklearn.metrics import balanced_accuracy_score, roc_auc_score + + out: Dict[str, float] = {} + logits = logits.detach().float().cpu() + y = targets.detach().cpu() + if target_type == "categorical": + y_int = y.long().view(-1) + out["loss_region"] = float(torch.nn.functional.cross_entropy(logits, y_int)) + probs = torch.softmax(logits, dim=1).numpy() + yi = y_int.numpy() + n_classes = probs.shape[1] + try: + if n_classes == 2: + out["auroc"] = float(roc_auc_score(yi, probs[:, 1])) + else: + out["auroc"] = float( + roc_auc_score(yi, probs, multi_class="ovr", average="macro") + ) + except ValueError: + out["auroc"] = float("nan") + pred = probs.argmax(axis=1) + out["bal_acc"] = ( + float(balanced_accuracy_score(yi, pred)) + if len(np.unique(yi)) > 1 + else float("nan") ) - self.log( - "val/r2", - torch.tensor(float(metrics["r2"]), device=self.device), - on_step=False, - on_epoch=True, - prog_bar=False, + out["pred_pos_frac"] = float((pred == 1).mean()) if n_classes == 2 else float("nan") + elif target_type == "binary": + yb = y.float().view(-1) + lg = logits.view(-1) + out["loss_region"] = float( + torch.nn.functional.binary_cross_entropy_with_logits(lg, yb) ) - elif target_type == "survival": - self.log( - "val/c_index", - torch.tensor(float(metrics["c_index"]), device=self.device), - on_step=False, - on_epoch=True, - prog_bar=True, + prob = torch.sigmoid(lg).numpy() + yi = yb.numpy().astype(int) + try: + out["auroc"] = float(roc_auc_score(yi, prob)) + except ValueError: + out["auroc"] = float("nan") + pred = (prob >= 0.5).astype(int) + out["bal_acc"] = ( + float(balanced_accuracy_score(yi, pred)) + if len(np.unique(yi)) > 1 + else float("nan") ) - self._reset_val_epoch_buffers() + out["pred_pos_frac"] = float(pred.mean()) + return out def on_train_epoch_end(self) -> None: if not self.region_accum_enabled: @@ -999,44 +1220,13 @@ def configure_optimizers(self): "(for example '+optim=adamw') or set model.optim explicitly." ) - optimization_cfg = dict(self.task_cfg.get("optimization", {})) - backbone_lr = optimization_cfg.get("backbone_lr") - attention_lr = optimization_cfg.get("attention_lr") - - params = [p for p in self.parameters() if p.requires_grad] - - if self.use_attention and backbone_lr is not None and attention_lr is not None: - seen: set[int] = set() - - def _collect_params(module) -> list[torch.nn.Parameter]: - out: list[torch.nn.Parameter] = [] - if module is None: - return out - for param in module.parameters(): - if not param.requires_grad: - continue - pid = id(param) - if pid in seen: - continue - seen.add(pid) - out.append(param) - return out - - backbone_params = _collect_params(self.encoder) + _collect_params(self.graph_head) - attention_params = _collect_params(self.attention) - param_groups: list[dict] = [] - if backbone_params: - param_groups.append({"params": backbone_params, "lr": float(backbone_lr)}) - if attention_params: - param_groups.append({"params": attention_params, "lr": float(attention_lr)}) - if param_groups: - params = param_groups - + params = self._build_optimizer_params() optimizer = instantiate_optimizer(self.optim_cfg, params) scheduler = instantiate_scheduler_with_warmup( self.scheduler_cfg, optimizer, warmup_cfg=self.task_cfg.get("lr_warmup"), + t_max_auto=self._resolve_t_max_auto(), ) if scheduler is None: return optimizer @@ -1044,11 +1234,119 @@ def _collect_params(module) -> list[torch.nn.Parameter]: "optimizer": optimizer, "lr_scheduler": { "scheduler": scheduler, - "interval": "epoch", + "interval": self.scheduler_interval, "frequency": 1, }, } + def _build_optimizer_params(self): + """Parameters or parameter groups for the optimizer. + + ``task.optimization`` accepts either the per-component multipliers + ``encoder_lr_scale`` / ``head_lr_scale`` / ``attention_lr_scale`` (with + ``head_weight_decay``), or the absolute pair ``backbone_lr`` + + ``attention_lr``. Setting only one of the absolute pair is an error rather + than a silent no-op. + """ + opt = self.optimization_cfg + scale_keys = { + "encoder_lr_scale", + "head_lr_scale", + "attention_lr_scale", + "head_weight_decay", + } + has_scales = any(opt.get(k) is not None for k in scale_keys) + backbone_lr = opt.get("backbone_lr") + attention_lr = opt.get("attention_lr") + if (backbone_lr is None) != (attention_lr is None): + raise ValueError( + "task.optimization.backbone_lr and attention_lr must be set together " + "(or use encoder_lr_scale / head_lr_scale / attention_lr_scale)." + ) + if has_scales and backbone_lr is not None: + raise ValueError( + "task.optimization mixes absolute (backbone_lr/attention_lr) and scaled " + "learning rates; use one scheme." + ) + + base_cfg = ( + OmegaConf.to_container(self.optim_cfg, resolve=True) + if isinstance(self.optim_cfg, DictConfig) + else dict(self.optim_cfg) + ) + base_lr = float(base_cfg.get("lr", 1e-3)) + base_wd = float(base_cfg.get("weight_decay", 0.0) or 0.0) + + if has_scales or self._encoder_frozen or self.freeze_head: + groups = build_param_groups( + base_lr=base_lr, + base_weight_decay=base_wd, + encoder=self.encoder, + head=self.graph_head, + attention=self.attention if self.use_attention else None, + encoder_lr_scale=0.0 + if self._encoder_frozen + else float(opt.get("encoder_lr_scale", 1.0) or 0.0), + head_lr_scale=float(opt.get("head_lr_scale", 1.0) or 0.0), + attention_lr_scale=float(opt.get("attention_lr_scale", 1.0) or 0.0), + head_weight_decay=opt.get("head_weight_decay"), + freeze_head=self.freeze_head, + ) + return groups + + params = [p for p in self.parameters() if p.requires_grad] + if self.use_attention and backbone_lr is not None: + # Absolute scheme: one backbone group (encoder + head) and one attention group. + seen: set[int] = set() + + def _collect(module) -> list[torch.nn.Parameter]: + out: list[torch.nn.Parameter] = [] + for p in module.parameters(): + if p.requires_grad and id(p) not in seen: + seen.add(id(p)) + out.append(p) + return out + + backbone = _collect(self.encoder) + _collect(self.graph_head) + attention = _collect(self.attention) + groups: list[dict] = [] + if backbone: + groups.append({"params": backbone, "lr": float(backbone_lr)}) + if attention: + groups.append({"params": attention, "lr": float(attention_lr)}) + return groups or params + return params + + def _resolve_t_max_auto(self) -> Optional[int]: + """Optimizer steps for ``scheduler.T_max: auto``.""" + try: + trainer = self.trainer + except RuntimeError: + return None + if trainer is None: + return None + max_epochs = int(trainer.max_epochs or 1) + if self.region_accum_enabled: + n_units = None + dm = getattr(trainer, "datamodule", None) + ds = getattr(dm, "dataset_train", None) if dm is not None else None + if ds is not None: + try: + n_units = len(ds) + except TypeError: + n_units = None + if n_units is None: + return None + return resolve_t_max_auto( + num_train_units=int(n_units), + max_epochs=max_epochs, + hyperbatch_size=self.region_accum_hyperbatch_size, + ) + try: + return int(trainer.estimated_stepping_batches) + except Exception: + return None + def _reset_val_epoch_buffers(self) -> None: self._val_bag_ids: list[str] = [] self._val_bag_logits_chunks: list[torch.Tensor] = [] diff --git a/src/grass_mil/models/training/__init__.py b/src/grass_mil/models/training/__init__.py index e847a6b..b839648 100644 --- a/src/grass_mil/models/training/__init__.py +++ b/src/grass_mil/models/training/__init__.py @@ -9,12 +9,14 @@ ) from .augmentations import ( build_augmentation, + build_finetune_augmentation, degree_importance, drop_edges, drop_features, drop_importance, feature_noise, ) +from .class_weights import collect_unit_labels, inverse_frequency_class_weights from .builders import ( build_supervised_components, infer_categorical_binding, @@ -33,9 +35,11 @@ compute_supervised_loss, ) from .optimization import ( + build_param_groups, instantiate_optimizer, instantiate_scheduler, instantiate_scheduler_with_warmup, + resolve_t_max_auto, ) from .ssl_runtime import CosineWarmup @@ -49,6 +53,11 @@ "select_target_columns", "build_supervised_components", "build_augmentation", + "build_finetune_augmentation", + "build_param_groups", + "collect_unit_labels", + "inverse_frequency_class_weights", + "resolve_t_max_auto", "degree_importance", "drop_edges", "drop_features", diff --git a/src/grass_mil/models/training/augmentations.py b/src/grass_mil/models/training/augmentations.py index 1cde83b..c157ec5 100644 --- a/src/grass_mil/models/training/augmentations.py +++ b/src/grass_mil/models/training/augmentations.py @@ -164,7 +164,9 @@ def drop_edges( if edge_index.numel() == 0: return out - draws = torch.rand((edge_index.size(1),), generator=generator, device=edge_index.device) + # Draw on the generator's device (CPU) and move the mask: a CPU generator + # cannot drive a CUDA draw, and fine-tuning batches live on the GPU. + draws = torch.rand((edge_index.size(1),), generator=generator).to(edge_index.device) keep = draws >= float(p) if force_undirected: keep = _mutual_edge_mask(edge_index, keep) @@ -176,18 +178,101 @@ def drop_edges( return out -def drop_features(data, *, p: float, generator: Optional[torch.Generator] = None): - """Zero whole feature columns with probability ``p``.""" - if p <= 0.0 or data.x is None or data.x.numel() == 0: +def drop_features( + data, + *, + p: float, + generator: Optional[torch.Generator] = None, + mode: str = "column", + categorical_column: Optional[int] = None, + unassigned_index: Optional[int] = None, +): + """Feature dropout. + + ``mode="column"`` zeroes whole feature columns with probability ``p`` (the + BGRL view generator). ``mode="elementwise"`` is the fine-tuning transform of + the preprint: every entry of ``x`` is zeroed independently with probability + ``p``, and a node whose draw falls below ``p`` has its categorical code + (column ``categorical_column`` of ``categorical_codes``) rewritten to + ``unassigned_index`` so the cell type is dropped as well. + """ + if p <= 0.0: return data + if mode not in {"column", "elementwise"}: + raise ValueError(f"drop_features mode must be 'column' or 'elementwise', got {mode!r}.") + if mode == "column": + if data.x is None or data.x.numel() == 0: + return data + out = _clone(data) + x = out.x.clone() + draws = torch.rand((x.size(1),), generator=generator).to(x.device) + x[:, draws < float(p)] = 0 + out.x = x + return out + out = _clone(data) - x = out.x.clone() - draws = torch.rand((x.size(1),), generator=generator, device=x.device) - x[:, draws < float(p)] = 0 - out.x = x + num_nodes = int(getattr(out, "num_nodes", 0) or (out.x.size(0) if out.x is not None else 0)) + if out.x is not None and out.x.numel() > 0: + x = out.x.clone() + draws = torch.rand(x.shape, generator=generator).to(x.device) + x[draws < float(p)] = 0 + out.x = x + codes = getattr(out, "categorical_codes", None) + if ( + unassigned_index is not None + and isinstance(codes, torch.Tensor) + and codes.numel() > 0 + and num_nodes > 0 + ): + col = 0 if categorical_column is None else int(categorical_column) + if col < codes.size(1): + node_draws = torch.rand((codes.size(0),), generator=generator).to(codes.device) + codes = codes.clone() + codes[node_draws < float(p), col] = int(unassigned_index) + out.categorical_codes = codes return out +def build_finetune_augmentation( + cfg: Optional[Mapping[str, Any]], + *, + unassigned_index: Optional[int] = None, + categorical_column: Optional[int] = None, + seed: Optional[int] = None, +) -> Optional[Callable[[Any], Any]]: + """Training-time augmentation for supervised fine-tuning. + + ``cfg`` keys: ``drop_edge_p`` (undirected edge drop, both directions kept + together) and ``drop_feat_p`` (element-wise feature drop that also writes the + unassigned cell-type token). Returns ``None`` when both are zero, so the + default fine-tuning path is untouched. + """ + cfg = dict(cfg or {}) + drop_edge_p = float(cfg.get("drop_edge_p", 0.0) or 0.0) + drop_feat_p = float(cfg.get("drop_feat_p", 0.0) or 0.0) + if drop_edge_p <= 0.0 and drop_feat_p <= 0.0: + return None + generator = torch.Generator() + generator.manual_seed(int(cfg.get("seed", seed if seed is not None else 0))) + + def _augment(data): + out = data + if drop_edge_p > 0.0: + out = drop_edges(out, p=drop_edge_p, force_undirected=True, generator=generator) + if drop_feat_p > 0.0: + out = drop_features( + out, + p=drop_feat_p, + generator=generator, + mode="elementwise", + categorical_column=categorical_column, + unassigned_index=unassigned_index, + ) + return out + + return _augment + + def feature_noise( data, *, diff --git a/src/grass_mil/models/training/builders.py b/src/grass_mil/models/training/builders.py index 57b5158..604c24c 100644 --- a/src/grass_mil/models/training/builders.py +++ b/src/grass_mil/models/training/builders.py @@ -138,15 +138,41 @@ def resolve_encoder_cfg( num_embeddings, column_index = ( infer_categorical_binding(module, label) if module is not None else (None, None) ) + label_map = infer_categorical_label_map(module, label) if module is not None else None resolved = resolve_categorical_embedding_config( categorical_cfg, cardinalities={label: num_embeddings} if num_embeddings is not None else None, slices={label: column_index} if column_index is not None else None, + label_maps={label: label_map} if label_map else None, ) cfg["categorical_embedding"] = asdict(resolved) return cfg +def infer_categorical_label_map( + module: Optional[L.LightningModule], label: str +) -> Optional[Dict[str, int]]: + """Return ``{category: code}`` for ``label`` from the attached datamodule, if any.""" + if module is None: + return None + try: + trainer = module.trainer + except RuntimeError: + return None + if trainer is None or trainer.datamodule is None: + return None + dataset = getattr(trainer.datamodule, "dataset_train", None) + if dataset is None: + return None + inner = dataset + while not hasattr(inner, "label_maps") and hasattr(inner, "dataset"): + inner = inner.dataset + label_maps = getattr(inner, "label_maps", None) + if isinstance(label_maps, dict) and label in label_maps: + return {str(k): int(v) for k, v in label_maps[label].items()} + return None + + def build_supervised_components( *, module: L.LightningModule, diff --git a/src/grass_mil/models/training/checkpoint_init.py b/src/grass_mil/models/training/checkpoint_init.py index a99715d..8a9adfc 100644 --- a/src/grass_mil/models/training/checkpoint_init.py +++ b/src/grass_mil/models/training/checkpoint_init.py @@ -34,8 +34,12 @@ def remap_encoder_keys( ) -> Dict[str, torch.Tensor]: if encoder_init_map == "identity": return dict(state_dict) + if encoder_init_map == "spacegm_bgrl": + return remap_spacegm_bgrl_keys(state_dict) if encoder_init_map != "auto_bgrl_or_identity": - raise ValueError("encoder_init_map must be one of ['identity', 'auto_bgrl_or_identity']") + raise ValueError( + "encoder_init_map must be one of ['identity', 'auto_bgrl_or_identity', 'spacegm_bgrl']" + ) remapped: Dict[str, torch.Tensor] = {} for key, value in state_dict.items(): new_key = key @@ -49,6 +53,51 @@ def remap_encoder_keys( return remapped +def remap_spacegm_bgrl_keys(state_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]: + """Import an encoder pretrained with the original SPACE-GM style BGRL code. + + Maps ``online_encoder.x_embedding`` -> ``encoder.input_proj.embedding``, + ``online_encoder.feat_embedding`` -> ``encoder.input_proj.linear``, + ``online_encoder.gnns.{i}.gine.{nn.0,nn.2,lin,eps}`` -> ``encoder.layers.{i}.*`` + and ``online_encoder.batch_norms.{i}`` -> ``encoder.norms.{i}``. The unused + GIN MLP copies, edge-type embeddings, the target encoder and the predictor + are dropped. The continuous input width must match the public model's + (``input_proj.linear.weight`` has one column per continuous node feature); + select columns beforehand if the legacy run carried masked extra columns. + """ + import re + + out: Dict[str, torch.Tensor] = {} + layer_re = re.compile( + r"^online_encoder\.gnns\.(\d+)\.gine\.(nn\.0|nn\.2|lin|eps)(\.(weight|bias))?$" + ) + norm_re = re.compile( + r"^online_encoder\.batch_norms\.(\d+)\.(weight|bias|running_mean|running_var|num_batches_tracked)$" + ) + for key, value in state_dict.items(): + if key == "online_encoder.x_embedding.weight": + out["encoder.input_proj.embedding.weight"] = value + continue + if key in ("online_encoder.feat_embedding.weight", "online_encoder.feat_embedding.bias"): + out[key.replace("online_encoder.feat_embedding", "encoder.input_proj.linear")] = value + continue + m = layer_re.match(key) + if m: + suffix = m.group(2) + (m.group(3) or "") + out[f"encoder.layers.{m.group(1)}.{suffix}"] = value + continue + m = norm_re.match(key) + if m: + out[f"encoder.norms.{m.group(1)}.{m.group(2)}"] = value + continue + # gnns.{i}.mlp.* (unused duplicate), edge_embedding, target_encoder, predictor: dropped + if not out: + raise ValueError( + "No SPACE-GM BGRL encoder keys (online_encoder.*) found in the checkpoint." + ) + return out + + def read_graph_head_bias( checkpoint_path: str, *, diff --git a/src/grass_mil/models/training/class_weights.py b/src/grass_mil/models/training/class_weights.py new file mode 100644 index 0000000..4957e0f --- /dev/null +++ b/src/grass_mil/models/training/class_weights.py @@ -0,0 +1,67 @@ +"""Class weights for the region cross-entropy, computed from the TRAINING split only. + +The preprint weights class ``c`` by inverse frequency on the training regions, +``w_c = n / (k * count_c)``, normalised to mean 1, so the weights change the +balance between classes but not the overall loss scale. Computing them on the +training split alone keeps the test fold's label prior out of the objective. +""" + +from __future__ import annotations + +from typing import Any, Iterable, Optional, Sequence + +import torch + +__all__ = ["inverse_frequency_class_weights", "collect_unit_labels"] + + +def inverse_frequency_class_weights( + labels: Sequence[int] | torch.Tensor, + num_classes: int, + *, + normalize_mean: bool = True, +) -> torch.Tensor: + """``w_c = n / (k * count_c)`` over ``labels`` (absent classes get weight 1).""" + if num_classes < 1: + raise ValueError("num_classes must be >= 1") + y = torch.as_tensor(list(labels) if not isinstance(labels, torch.Tensor) else labels) + y = y.long().view(-1) + if y.numel() == 0: + return torch.ones(num_classes, dtype=torch.float32) + counts = torch.bincount(y, minlength=num_classes).float() + weights = torch.ones(num_classes, dtype=torch.float32) + present = counts > 0 + weights[present] = float(y.numel()) / (float(int(present.sum())) * counts[present]) + if normalize_mean: + weights = weights / weights.mean() + return weights + + +def collect_unit_labels( + dataset: Iterable[Any], + *, + target_columns: Optional[Sequence[str]] = None, +) -> list[int]: + """Read the integer class label of every unit of a graph dataset. + + Units carry ``graph_y`` (shape ``[1, T]`` or ``[T]``); the first target column + is used unless ``target_columns`` names one and the unit exposes + ``graph_label_names``. + """ + labels: list[int] = [] + for data in dataset: + y = getattr(data, "graph_y", None) + if y is None: + continue + y = torch.as_tensor(y).view(-1) + col = 0 + names = getattr(data, "graph_label_names", None) + if target_columns and isinstance(names, (list, tuple)): + flat = list(names[0]) if names and isinstance(names[0], (list, tuple)) else list(names) + if target_columns[0] in flat: + col = int(flat.index(target_columns[0])) + value = float(y[col]) + if value != value: # NaN: unlabelled unit + continue + labels.append(int(round(value))) + return labels diff --git a/src/grass_mil/models/training/optimization.py b/src/grass_mil/models/training/optimization.py index 6efe834..ebaae01 100644 --- a/src/grass_mil/models/training/optimization.py +++ b/src/grass_mil/models/training/optimization.py @@ -28,28 +28,121 @@ def instantiate_optimizer( def instantiate_scheduler( cfg: Optional[Dict[str, Any] | DictConfig], optimizer: torch.optim.Optimizer, + *, + t_max_auto: Optional[int] = None, ): + """Instantiate a scheduler; ``T_max: auto`` is replaced by ``t_max_auto`` steps.""" if cfg is None: return None sch_cfg = cfg if isinstance(cfg, DictConfig) else OmegaConf.create(cfg) + resolved = OmegaConf.to_container(sch_cfg, resolve=True) + if isinstance(resolved, dict) and str(resolved.get("T_max", "")).lower() == "auto": + if t_max_auto is None or int(t_max_auto) < 1: + raise ValueError( + "scheduler.T_max='auto' needs the number of optimizer steps; the module " + "computes it from max_epochs, the training set size and the hyperbatch." + ) + resolved["T_max"] = int(t_max_auto) + sch_cfg = OmegaConf.create(resolved) return hydra.utils.instantiate(sch_cfg, optimizer=optimizer) +def resolve_t_max_auto(*, num_train_units: int, max_epochs: int, hyperbatch_size: int) -> int: + """Optimizer steps of a run: ``max_epochs * ceil(num_train_units / hyperbatch)``. + + The preprint anneals the cosine schedule over every optimizer step of the + run (``T_max = epochs * n_train // hyperbatch``), so the learning rate never + restarts. + """ + hb = max(1, int(hyperbatch_size)) + steps_per_epoch = max(1, (int(num_train_units) + hb - 1) // hb) + return max(1, int(max_epochs)) * steps_per_epoch + + +def build_param_groups( + *, + base_lr: float, + base_weight_decay: float, + encoder: Optional[torch.nn.Module], + head: Optional[torch.nn.Module], + attention: Optional[torch.nn.Module], + encoder_lr_scale: float = 1.0, + head_lr_scale: float = 1.0, + attention_lr_scale: float = 1.0, + head_weight_decay: Optional[float] = None, + freeze_head: bool = False, +) -> list[dict]: + """Parameter groups with per-component learning-rate multipliers. + + A scale of 0 removes the component from the optimizer (the caller also sets + ``requires_grad=False`` and keeps it in eval mode). Groups are ordered + encoder, head, attention; ``head_weight_decay`` overrides the optimizer's + weight decay for the head group only (the preprint uses 1e-3 on a pre-fitted + linear head). + """ + seen: set[int] = set() + + def _params(module: Optional[torch.nn.Module]) -> list[torch.nn.Parameter]: + out: list[torch.nn.Parameter] = [] + if module is None: + return out + for p in module.parameters(): + if not p.requires_grad or id(p) in seen: + continue + seen.add(id(p)) + out.append(p) + return out + + groups: list[dict] = [] + if encoder is not None and float(encoder_lr_scale) > 0.0: + enc = _params(encoder) + if enc: + groups.append( + {"params": enc, "lr": float(base_lr) * float(encoder_lr_scale), "name": "encoder"} + ) + if head is not None and not freeze_head and float(head_lr_scale) > 0.0: + hp = _params(head) + if hp: + group = {"params": hp, "lr": float(base_lr) * float(head_lr_scale), "name": "head"} + if head_weight_decay is not None: + group["weight_decay"] = float(head_weight_decay) + groups.append(group) + if attention is not None and float(attention_lr_scale) > 0.0: + ap = _params(attention) + if ap: + groups.append( + { + "params": ap, + "lr": float(base_lr) * float(attention_lr_scale), + "name": "attention", + } + ) + if not groups: + raise ValueError( + "No trainable parameters: every component is frozen or has a zero learning-rate scale." + ) + for g in groups: + g.setdefault("weight_decay", float(base_weight_decay)) + return groups + + def instantiate_scheduler_with_warmup( cfg: Optional[Dict[str, Any] | DictConfig], optimizer: torch.optim.Optimizer, warmup_cfg: Optional[Dict[str, Any] | DictConfig] = None, + *, + t_max_auto: Optional[int] = None, ): warm_cfg = ( warmup_cfg if isinstance(warmup_cfg, DictConfig) else OmegaConf.create(warmup_cfg or {}) ) warmup_enabled = bool(warm_cfg.get("enabled", False)) if not warmup_enabled: - return instantiate_scheduler(cfg, optimizer) + return instantiate_scheduler(cfg, optimizer, t_max_auto=t_max_auto) warmup_steps = int(warm_cfg.get("warmup_steps", 0)) if warmup_steps <= 0: - return instantiate_scheduler(cfg, optimizer) + return instantiate_scheduler(cfg, optimizer, t_max_auto=t_max_auto) start_factor = float(warm_cfg.get("start_factor", 0.1)) if start_factor <= 0: @@ -64,7 +157,7 @@ def instantiate_scheduler_with_warmup( total_iters=warmup_steps, ) - base_scheduler = instantiate_scheduler(cfg, optimizer) + base_scheduler = instantiate_scheduler(cfg, optimizer, t_max_auto=t_max_auto) if base_scheduler is None: return warmup_scheduler diff --git a/src/grass_mil/models/training/probe.py b/src/grass_mil/models/training/probe.py new file mode 100644 index 0000000..db376c1 --- /dev/null +++ b/src/grass_mil/models/training/probe.py @@ -0,0 +1,275 @@ +"""Linear probe pre-fit on a frozen encoder (preprint "probe head"). + +The preprint initialises the linear instance head of the probehead / ftALL +regimes from a logistic regression fitted on ego-graph embeddings of the +frozen (BGRL-pretrained) encoder: every ego-graph inherits the label of its +region, a ``StandardScaler`` + ``LogisticRegression`` is fitted on the training +regions, and the scaler is folded into the weights so the head acts on raw +embeddings. For a binary task the class-0 row stays zero (the logistic decision +function becomes the class-1 logit); for more classes the multinomial +coefficients are used directly. + +Nothing here is specific to the preprint sizes: the number of roots per region, +the seed, the regulariser and the ego-graph settings are all arguments (the CLI +reads them from ``configs/probe.yaml`` and from ``data.sampler``). +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Dict, List, Optional, Sequence + +import numpy as np +import torch + +__all__ = [ + "ProbeEmbeddings", + "ProbeFit", + "attach_datamodule", + "collect_frozen_embeddings", + "fit_probe", + "fold_probe_into_linear_head", + "probe_sampler_config", + "region_mean_auroc", + "save_probe_head", + "load_probe_head", +] + + +@dataclass +class ProbeEmbeddings: + """Ego-graph embeddings with their region ids and inherited region labels.""" + + embeddings: np.ndarray # (n_instances, dim) + labels: np.ndarray # (n_instances,) int + region_ids: np.ndarray # (n_instances,) str + label_by_region: Dict[str, int] = field(default_factory=dict) + + +@dataclass +class ProbeFit: + weight: torch.Tensor # (num_classes, dim), scaler folded in + bias: torch.Tensor # (num_classes,) + metrics: Dict[str, float] = field(default_factory=dict) + settings: Dict[str, Any] = field(default_factory=dict) + + +def attach_datamodule(model: Any, datamodule: Any, trainer: Any) -> None: + """Bind a datamodule to a model outside ``Trainer.fit`` so ``setup`` can inspect it. + + ``SupervisedModule.setup`` infers the encoder input width and the categorical + vocabulary from ``trainer.datamodule``; this mirrors what Lightning does at + the start of ``fit``/``test``. + """ + trainer._data_connector.attach_data(model, datamodule=datamodule) + model.trainer = trainer + datamodule.trainer = trainer + + +def probe_sampler_config( + base: Any, + *, + roots_per_unit: Optional[int], + seed: int, + batch_size: int, +) -> Any: + """Evaluation sampler for the probe: fixed uniform roots per region, no bagging. + + ``base`` is the datamodule's training ``SamplerConfig``; its ego-graph settings + (depth, neighbour limit, radius cap, interior seeds) are kept. + """ + from grass_mil.data.components.samplers import SamplerConfig + + if base is None or getattr(base, "name", "identity") == "identity": + raise ValueError( + "The probe needs an ego-graph sampler (data.sampler.name=shadow_custom with " + "runtime.enabled=true, e.g. data/sampler=ego_graph_mil)." + ) + runtime = dict(base.runtime or {}) + runtime.update( + { + "enabled": True, + "subsample_fraction": 1.0, + "subsample_count": int(roots_per_unit) if roots_per_unit else None, + "subsample_seed": None, + "fixed_per_unit_seed": int(seed), + "proportional_root_sampling": False, + "unit_as_bag": False, + "merge_units": False, + "shuffle_subgraphs": False, + "subgraph_batch_size": int(batch_size), + } + ) + return SamplerConfig(name=base.name, kwargs=dict(base.kwargs or {}), runtime=runtime) + + +def _region_ids_of(batch: Any, n: int) -> List[str]: + from grass_mil.models.training.bagging import extract_bag_ids + + ids = extract_bag_ids(batch, bag_key="region_id", bag_fallback_key="sample_id") + if len(ids) == 1 and n > 1: + ids = ids * n + if len(ids) != n: + raise ValueError(f"Batch carries {len(ids)} region ids for {n} ego-graphs.") + return [str(v) for v in ids] + + +def _labels_of(batch: Any, n: int, target_columns: Optional[Sequence[str]]) -> np.ndarray: + from grass_mil.models.training.bagging import select_target_columns + + if not hasattr(batch, "graph_y"): + raise ValueError("Batch has no graph_y; the probe needs region labels.") + y = select_target_columns(batch.graph_y.float(), batch, target_columns) + y = y.view(y.shape[0], -1)[:, 0] + if y.shape[0] == 1 and n > 1: + y = y.expand(n) + return np.rint(y.detach().cpu().numpy()).astype(int) + + +@torch.no_grad() +def collect_frozen_embeddings( + model: Any, + dataset: Any, + *, + sampler_config: Any, + target_columns: Optional[Sequence[str]] = None, + device: Optional[torch.device | str] = None, + num_workers: int = 0, +) -> ProbeEmbeddings: + """Encode ``roots_per_unit`` ego-graphs per region with the frozen encoder.""" + from grass_mil.data.components.samplers import get_sampler_strategy + + strategy = get_sampler_strategy(sampler_config) + loader = strategy.build_dataset_loader( + dataset=dataset, batch_size=1, num_workers=num_workers, pin_memory=False, shuffle=False + ) + device = torch.device(device) if device is not None else next(model.parameters()).device + was_training = model.training + model.eval() + embs: List[np.ndarray] = [] + labels: List[np.ndarray] = [] + rids: List[np.ndarray] = [] + for batch in loader: + batch = batch.to(device) + _, graph_emb, _ = model._forward_instances(batch) + n = int(graph_emb.shape[0]) + embs.append(graph_emb.detach().float().cpu().numpy()) + labels.append(_labels_of(batch, n, target_columns)) + rids.append(np.asarray(_region_ids_of(batch, n), dtype=object)) + if was_training: + model.train() + if not embs: + raise ValueError("No ego-graphs were produced; is the dataset split empty?") + e = np.concatenate(embs) + y = np.concatenate(labels) + r = np.concatenate(rids) + by_region = {str(k): int(v) for k, v in zip(r, y)} + return ProbeEmbeddings(embeddings=e, labels=y, region_ids=r, label_by_region=by_region) + + +def region_mean_auroc(prob: np.ndarray, region_ids: np.ndarray, label_by_region: Dict[str, int]): + """AUROC of the per-region mean probability (how the preprint scores the probe).""" + from sklearn.metrics import roc_auc_score + + regions = np.unique(region_ids) + means = np.array([float(prob[region_ids == r].mean()) for r in regions]) + y = np.array([label_by_region[str(r)] for r in regions]) + if len(np.unique(y)) < 2: + return float("nan") + return float(roc_auc_score(y, means)) + + +def fold_probe_into_linear_head( + coef: np.ndarray, + intercept: np.ndarray, + scaler_mean: np.ndarray, + scaler_scale: np.ndarray, + *, + num_classes: int, +) -> tuple[torch.Tensor, torch.Tensor]: + """Fold ``StandardScaler`` + logistic coefficients into ``Linear(dim, num_classes)``. + + Binary (``coef`` has one row): the class-1 row carries the decision function and + the class-0 row is zero, so ``softmax`` over the two logits equals the logistic + probability. Multinomial: rows map one-to-one onto classes. + """ + coef = np.asarray(coef, dtype=np.float64) + intercept = np.asarray(intercept, dtype=np.float64).reshape(-1) + w = coef / scaler_scale[None, :] + b = intercept - (coef * scaler_mean[None, :] / scaler_scale[None, :]).sum(axis=1) + dim = coef.shape[1] + W = torch.zeros(num_classes, dim, dtype=torch.float32) + B = torch.zeros(num_classes, dtype=torch.float32) + if coef.shape[0] == 1: + if num_classes < 2: + raise ValueError("A binary probe needs a 2-logit head (graph_head.output_dim=2).") + W[1] = torch.from_numpy(w[0].astype(np.float32)) + B[1] = float(b[0]) + else: + if coef.shape[0] != num_classes: + raise ValueError( + f"Probe has {coef.shape[0]} classes but the head has {num_classes} outputs." + ) + W[:] = torch.from_numpy(w.astype(np.float32)) + B[:] = torch.from_numpy(b.astype(np.float32)) + return W, B + + +def fit_probe( + train: ProbeEmbeddings, + *, + num_classes: int, + C: float = 1.0, + class_weight: Optional[str] = "balanced", + max_iter: int = 3000, + eval_sets: Optional[Dict[str, ProbeEmbeddings]] = None, +) -> ProbeFit: + """Fit the scaler + logistic regression and fold it into head weights.""" + from sklearn.linear_model import LogisticRegression + from sklearn.metrics import roc_auc_score + from sklearn.preprocessing import StandardScaler + + if len(np.unique(train.labels)) < 2: + raise ValueError("The probe training set has a single class.") + scaler = StandardScaler().fit(train.embeddings) + clf = LogisticRegression(max_iter=int(max_iter), C=float(C), class_weight=class_weight) + clf.fit(scaler.transform(train.embeddings), train.labels) + W, B = fold_probe_into_linear_head( + clf.coef_, clf.intercept_, scaler.mean_, scaler.scale_, num_classes=num_classes + ) + metrics: Dict[str, float] = {"n_train_instances": float(len(train.labels))} + for name, emb in (eval_sets or {}).items(): + prob = clf.predict_proba(scaler.transform(emb.embeddings)) + if prob.shape[1] == 2: + p1 = prob[:, 1] + if len(np.unique(emb.labels)) >= 2: + metrics[f"{name}_instance_auroc"] = float(roc_auc_score(emb.labels, p1)) + metrics[f"{name}_region_auroc"] = region_mean_auroc( + p1, emb.region_ids, emb.label_by_region + ) + metrics[f"n_{name}_instances"] = float(len(emb.labels)) + settings = {"C": float(C), "class_weight": class_weight, "max_iter": int(max_iter)} + return ProbeFit(weight=W, bias=B, metrics=metrics, settings=settings) + + +def save_probe_head( + fit: ProbeFit, path: str | Path, extra: Optional[Dict[str, Any]] = None +) -> Path: + """Write the folded head as ``graph_head.net.0.{weight,bias}`` (+ metrics).""" + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + payload: Dict[str, Any] = { + "graph_head.net.0.weight": fit.weight.clone(), + "graph_head.net.0.bias": fit.bias.clone(), + "metrics": dict(fit.metrics), + "settings": dict(fit.settings), + } + if extra: + payload.update(extra) + torch.save(payload, path) + return path + + +def load_probe_head(path: str | Path) -> Dict[str, Any]: + return torch.load(Path(path), map_location="cpu", weights_only=False) diff --git a/src/grass_mil/nested_cv.py b/src/grass_mil/nested_cv.py new file mode 100644 index 0000000..4edd848 --- /dev/null +++ b/src/grass_mil/nested_cv.py @@ -0,0 +1,532 @@ +"""``grass-mil-nested-cv``: nested, patient-grouped cross-validation (preprint protocol). + +Per outer fold ``i`` (``folds_file``): development = all other folds, split into +``inner_k`` stratified patient-grouped inner folds; every inner run trains the +selected regime and reports its best epoch; the refit trains on the whole +development set for the median best epoch and is scored once on fold ``i`` with +every cell of every region. One out-of-fold prediction per region and a mean +per patient are written alongside the per-fold summary. +""" + +from __future__ import annotations + +import copy +import hashlib +import json +import statistics +from pathlib import Path +from typing import Any, Dict, List, Optional, Sequence + +import hydra +import lightning as L +import numpy as np +import pandas as pd +from omegaconf import DictConfig, open_dict + +from grass_mil._paths import setup_project_root + +setup_project_root() + +from grass_mil.data.components.splits import ( # noqa: E402 + LabelTable, + load_fold_json, + load_labels, + make_inner_folds, +) +from grass_mil.train import train # noqa: E402 +from grass_mil.utils import RankedLogger, extras # noqa: E402 + +log = RankedLogger(__name__, rank_zero_only=True) + +__all__ = ["run_nested_cv", "best_epoch_from_jsonl", "main"] + + +# ---------------------------------------------------------------------------- +# helpers +# ---------------------------------------------------------------------------- + + +def best_epoch_from_jsonl( + path: str | Path, + *, + metric: str, + mode: str = "max", + fallback_metric: Optional[str] = "val/loss_region", +) -> Optional[tuple[int, str]]: + """1-based number of epochs reaching the best ``metric`` (earliest on ties). + + Returns ``(epochs, metric_used)``. When ``metric`` was never logged (e.g. the + validation fold holds a single class, so AUROC is undefined) the + ``fallback_metric`` is minimised instead, and failing that the last epoch is + returned with ``"last"``. + """ + path = Path(path) + if not path.exists(): + return None + rows = [json.loads(line) for line in path.read_text().splitlines() if line.strip()] + if not rows: + return None + usable = [r for r in rows if r.get(metric) is not None] + if usable: + sign = 1.0 if mode == "max" else -1.0 + best = max(usable, key=lambda r: (sign * float(r[metric]), -int(r["epoch"]))) + return int(best["epoch"]) + 1, metric + if fallback_metric: + usable = [r for r in rows if r.get(fallback_metric) is not None] + if usable: + best = min(usable, key=lambda r: (float(r[fallback_metric]), int(r["epoch"]))) + return int(best["epoch"]) + 1, fallback_metric + return int(max(int(r["epoch"]) for r in rows)) + 1, "last" + + +def _reduce_epochs(epochs: Sequence[int], how: str, minimum: int) -> int: + if not epochs: + raise ValueError("No inner-fold epochs to reduce.") + how = str(how).lower() + if how == "median": + value = statistics.median(epochs) + elif how == "mean": + value = statistics.mean(epochs) + elif how == "max": + value = max(epochs) + else: + raise ValueError("nested_cv.epoch_reduce must be median, mean or max.") + return max(int(minimum), int(value)) + + +def _resolve_label_column(cfg: DictConfig) -> str: + explicit = cfg.nested_cv.get("label_column") + if explicit: + return str(explicit) + targets = cfg.task.get("target_columns") + if targets: + return str(list(targets)[0]) + tasks = cfg.data.graph_labels.get("tasks") + if tasks: + return str(list(tasks)[0]) + raise ValueError( + "Set nested_cv.label_column (or task.target_columns / data.graph_labels.tasks) " + "so the driver knows which label column stratifies the inner folds." + ) + + +def _probe_mode(cfg: DictConfig) -> str: + """'always' | 'never' | 'auto' -> resolved to whether a probe is needed.""" + enabled = cfg.nested_cv.probe.get("enabled", "auto") + if isinstance(enabled, bool): + return "always" if enabled else "never" + enabled = str(enabled).lower() + if enabled not in {"auto", "true", "false"}: + raise ValueError("nested_cv.probe.enabled must be auto, true or false.") + if enabled == "auto": + return "auto" + return "always" if enabled == "true" else "never" + + +def _needs_probe(cfg: DictConfig) -> bool: + mode = _probe_mode(cfg) + if mode == "always": + return True + if mode == "never": + return False + return str(cfg.task.get("head_init_path") or "").strip().lower() == "auto" + + +def _split_hash(train_ids: Sequence[str], cfg: DictConfig) -> str: + h = hashlib.sha1() + h.update("|".join(sorted(str(r) for r in train_ids)).encode()) + h.update(str(cfg.model.get("init_from_ckpt")).encode()) + p = cfg.nested_cv.probe + h.update(f"{p.roots_per_unit}|{p.seed}|{p.C}|{p.class_weight}".encode()) + h.update(f"{cfg.data.sampler.runtime.depth}|{cfg.data.sampler.runtime.num_neighbors}".encode()) + return h.hexdigest()[:10] + + +def _id_kind(cfg: DictConfig) -> str: + kind = str(cfg.nested_cv.get("id_kind", "region")).lower() + if kind not in {"region", "sample", "patch"}: + raise ValueError("nested_cv.id_kind must be region, sample or patch") + return kind + + +def _with_split(cfg: DictConfig, *, train_ids, val_ids, test_ids, output_dir: Path) -> DictConfig: + fold_cfg = copy.deepcopy(cfg) + kind = _id_kind(cfg) + with open_dict(fold_cfg): + fold_cfg.data.split.explicit = { + "train": [str(r) for r in train_ids], + "val": [str(r) for r in val_ids], + "test": [str(r) for r in test_ids], + "id_kind": kind, + } + if kind == "patch": + fold_cfg.task.bag_key = "patch_id" + fold_cfg.data.graph_labels.scope = "patch" + elif kind == "sample": + fold_cfg.task.bag_key = "sample_id" + fold_cfg.data.graph_labels.scope = "sample" + fold_cfg.paths.output_dir = str(output_dir) + fold_cfg.trainer.default_root_dir = str(output_dir) + return fold_cfg + + +def _fit_probe_for(cfg: DictConfig, *, train_ids, val_ids, tag: str) -> Path: + from grass_mil.probe import run_probe + + cache_dir = Path(str(cfg.nested_cv.probe.cache_dir)) + cache_dir.mkdir(parents=True, exist_ok=True) + out = cache_dir / f"probe_{tag}_{_split_hash(train_ids, cfg)}.pt" + if out.exists() and bool(cfg.nested_cv.get("resume", True)): + log.info(f"[{tag}] probe cached: {out}") + return out + probe_cfg = _with_split( + cfg, train_ids=train_ids, val_ids=val_ids, test_ids=val_ids, output_dir=cache_dir / tag + ) + with open_dict(probe_cfg): + probe_cfg.task.head_init_path = None + probe_cfg.probe = { + "roots_per_unit": cfg.nested_cv.probe.roots_per_unit, + "seed": cfg.nested_cv.probe.seed, + "batch_size": cfg.nested_cv.probe.batch_size, + "C": cfg.nested_cv.probe.C, + "class_weight": cfg.nested_cv.probe.class_weight, + "max_iter": cfg.nested_cv.probe.max_iter, + "eval_splits": ["val"], + "output_path": str(out), + "device": cfg.nested_cv.probe.device, + } + run_probe(probe_cfg) + return out + + +def _train_run( + cfg: DictConfig, + *, + train_ids, + val_ids, + test_ids, + output_dir: Path, + head_init: Optional[Path], + max_epochs: Optional[int] = None, + refit: bool = False, + test: bool = False, +) -> Dict[str, Any]: + done_path = output_dir / "done.json" + if done_path.exists() and bool(cfg.nested_cv.get("resume", True)): + if not test or _predictions_cover(output_dir, test_ids, _id_kind(cfg)): + log.info(f"resume: {output_dir} already finished") + return json.loads(done_path.read_text()) + log.warning(f"resume: {output_dir} has stale test predictions; rerunning") + fold_cfg = _with_split( + cfg, train_ids=train_ids, val_ids=val_ids, test_ids=test_ids, output_dir=output_dir + ) + with open_dict(fold_cfg): + fold_cfg.task.head_init_path = str(head_init) if head_init is not None else None + fold_cfg.train = True + fold_cfg.test = bool(test) + fold_cfg.task_name = f"{cfg.task_name}_{output_dir.name}" + if max_epochs is not None: + fold_cfg.trainer.max_epochs = int(max_epochs) + fold_cfg.trainer.min_epochs = int(max_epochs) + if refit and fold_cfg.get("callbacks"): + # Fixed-length refit: no early stopping, no metric-based selection; + # the last epoch's weights are the model. + cbs = fold_cfg.callbacks + if "early_stopping" in cbs: + del cbs["early_stopping"] + if "model_checkpoint" in cbs and cbs.model_checkpoint is not None: + cbs.model_checkpoint.monitor = None + cbs.model_checkpoint.save_top_k = 0 + cbs.model_checkpoint.save_last = True + cbs.model_checkpoint.filename = "last_epoch" + metrics, _objects = train(fold_cfg) + numeric = {} + for k, v in metrics.items(): + try: + numeric[k] = float(v.item() if hasattr(v, "item") else v) + except Exception: + continue + result = { + "output_dir": str(output_dir), + "metrics": numeric, + "n_train": len(train_ids), + "n_val": len(val_ids), + "n_test": len(test_ids), + "max_epochs": int(fold_cfg.trainer.max_epochs), + "head_init_path": str(head_init) if head_init is not None else None, + } + best = best_epoch_from_jsonl( + output_dir / "metrics_per_epoch.jsonl", + metric=str(cfg.nested_cv.selection_metric), + mode=str(cfg.nested_cv.selection_mode), + ) + result["best_epoch"] = best[0] if best else None + result["selection_metric_used"] = best[1] if best else None + done_path.write_text(json.dumps(result, indent=1)) + return result + + +def _unit_ids(df: pd.DataFrame, kind: str) -> pd.Series: + """Unit id of every prediction row. + + Region bags carry their region id in ``region_id``; patch and sample bags + are identified by the bag id, which the bagging code namespaces as + ``sample::region::patch`` (or ``sample::patch``) for patches, so the unit is + its last segment. + """ + if kind == "region": + return df["region_id"].astype(str) + return df["bag_id"].astype(str).str.split("::").str[-1] + + +def _predictions_cover(output_dir: Path, test_ids, kind: str) -> bool: + """True when the run's test predictions hold exactly the expected unit ids.""" + path = output_dir / "test_predictions.csv" + if not path.exists(): + return False + df = pd.read_csv(path) + if ("region_id" if kind == "region" else "bag_id") not in df.columns: + return False + return set(_unit_ids(df, kind)) == {str(r) for r in test_ids} + + +def _read_test_predictions(output_dir: Path) -> pd.DataFrame: + path = output_dir / "test_predictions.csv" + if not path.exists(): + raise FileNotFoundError(f"Refit run wrote no test predictions: {path}") + return pd.read_csv(path) + + +def _auroc(y: Sequence[int], score: Sequence[float]) -> float: + from sklearn.metrics import roc_auc_score + + y = np.asarray(y) + if len(np.unique(y)) < 2: + return float("nan") + return float(roc_auc_score(y, np.asarray(score, dtype=float))) + + +# ---------------------------------------------------------------------------- +# driver +# ---------------------------------------------------------------------------- + + +def run_nested_cv(cfg: DictConfig) -> Dict[str, Any]: + if cfg.get("seed"): + L.seed_everything(cfg.seed, workers=True) + ncfg = cfg.nested_cv + base_out = Path(str(cfg.paths.output_dir)) + base_out.mkdir(parents=True, exist_ok=True) + + label_column = _resolve_label_column(cfg) + patient_column = ncfg.get("patient_column") + table: LabelTable = load_labels( + str(ncfg.label_file), + id_column=str(ncfg.id_column), + label_column=label_column, + patient_column=str(patient_column) if patient_column else None, + ) + folds = load_fold_json(str(ncfg.folds_file)) + labelled = set(table.labels.index) + dropped = sum(len([r for r in f if r not in labelled]) for f in folds) + if dropped: + log.warning(f"{dropped} region(s) in the folds have no '{label_column}' label; dropped") + folds = [[r for r in f if r in labelled] for f in folds] + if any(len(f) == 0 for f in folds): + raise ValueError("An outer fold is empty after dropping unlabelled regions.") + + # Precompute once (the inner runs re-partition the same processed graphs). + datamodule = hydra.utils.instantiate(cfg.data) + datamodule.prepare_data() + + needs_probe = _needs_probe(cfg) + if needs_probe and int(cfg.model.graph_head.get("num_layers", 2)) != 1: + raise ValueError( + "A probe pre-fit targets a linear instance head; set model.graph_head.num_layers=1 " + "(the linear_* regimes do) or nested_cv.probe.enabled=false." + ) + if not needs_probe and str(cfg.task.get("head_init_path") or "").lower() == "auto": + raise ValueError( + "task.head_init_path='auto' needs a probe but nested_cv.probe.enabled=false; " + "pass an explicit task.head_init_path or enable the probe." + ) + fixed_head = None + if not needs_probe and cfg.task.get("head_init_path"): + fixed_head = Path(str(cfg.task.head_init_path)) + log.warning(f"Using one fixed head init for every fold: {fixed_head}") + + selected = ncfg.get("outer_folds") + outer_indices = list(range(len(folds))) if not selected else [int(i) for i in selected] + inner_k = int(ncfg.inner_k) + regime = str(cfg.get("regime_name", "custom")) + log.info( + f"Nested CV [{regime}]: {len(folds)} outer folds ({len(outer_indices)} selected), " + f"inner k={inner_k} seed={int(ncfg.inner_seed)}, probe={'yes' if needs_probe else 'no'}" + ) + + fold_results: List[Dict[str, Any]] = [] + oof_frames: List[pd.DataFrame] = [] + for i in outer_indices: + outer_dir = base_out / f"outer_{i}" + outer_dir.mkdir(parents=True, exist_ok=True) + result_path = outer_dir / "result.json" + if result_path.exists() and bool(ncfg.get("resume", True)): + res = json.loads(result_path.read_text()) + log.info(f"[outer {i}] resume: AUROC {res['test_auroc']:.4f} (E={res['E']})") + fold_results.append(res) + oof_frames.append(pd.read_csv(outer_dir / "oof_predictions.csv")) + continue + + test_ids = list(folds[i]) + dev_ids = sorted(r for j, f in enumerate(folds) if j != i for r in f) + inner = make_inner_folds(dev_ids, table, k=inner_k, seed=int(ncfg.inner_seed)) + + inner_epochs: List[int] = [] + inner_summaries: List[Dict[str, Any]] = [] + for j, (tr, va) in enumerate(inner): + tag = f"o{i}_i{j}" + head = fixed_head + if needs_probe: + head = _fit_probe_for(cfg, train_ids=tr, val_ids=va, tag=tag) + res = _train_run( + cfg, + train_ids=tr, + val_ids=va, + test_ids=test_ids, + output_dir=outer_dir / f"inner_{j}", + head_init=head, + test=False, + ) + if res.get("best_epoch") is None: + raise RuntimeError( + f"[{tag}] no '{ncfg.selection_metric}' in metrics_per_epoch.jsonl; " + "is validation enabled with region-level metrics?" + ) + used = res.get("selection_metric_used") + if used != str(ncfg.selection_metric): + log.warning( + f"[{tag}] '{ncfg.selection_metric}' was never logged (single-class " + f"validation fold?); epoch chosen by '{used}'" + ) + inner_epochs.append(int(res["best_epoch"])) + inner_summaries.append( + { + "inner": j, + "best_epoch": res["best_epoch"], + "selection_metric_used": used, + "n_train": len(tr), + "n_val": len(va), + } + ) + log.info(f"[{tag}] best epoch {res['best_epoch']} ({used})") + + E = _reduce_epochs(inner_epochs, str(ncfg.epoch_reduce), int(ncfg.min_refit_epochs)) + tag = f"o{i}_refit" + head = fixed_head + if needs_probe: + head = _fit_probe_for(cfg, train_ids=dev_ids, val_ids=test_ids, tag=tag) + res = _train_run( + cfg, + train_ids=dev_ids, + val_ids=test_ids, + test_ids=test_ids, + output_dir=outer_dir / "refit", + head_init=head, + max_epochs=E, + refit=True, + test=True, + ) + preds = _read_test_predictions(outer_dir / "refit") + preds.insert(0, "outer", i) + kind = _id_kind(cfg) + if kind != "region": + # keep the unit id (patch or sample) in region_id so every + # downstream table has one id column + preds["region_id"] = _unit_ids(preds, kind) + preds["patient_id"] = [table.group_of(r) for r in preds["region_id"].astype(str)] + preds.to_csv(outer_dir / "oof_predictions.csv", index=False) + score_col = "prob_class_1" if "prob_class_1" in preds.columns else None + test_auroc = res["metrics"].get("test/auroc") + if test_auroc is None and score_col is not None: + test_auroc = _auroc(preds["label"], preds[score_col]) + fold_res = { + "outer": i, + "E": E, + "inner_epochs": inner_epochs, + "inner": inner_summaries, + "test_auroc": float(test_auroc) if test_auroc is not None else float("nan"), + "test_bal_acc": res["metrics"].get("test/bal_acc"), + "n_test": len(test_ids), + "n_test_evaluated": int(preds["region_id"].nunique()), + "n_dev": len(dev_ids), + "run_dir": str(outer_dir / "refit"), + "head_init_path": res.get("head_init_path"), + } + if fold_res["n_test_evaluated"] != len(test_ids): + log.warning( + f"[outer {i}] evaluated {fold_res['n_test_evaluated']} of {len(test_ids)} regions" + ) + result_path.write_text(json.dumps(fold_res, indent=1)) + fold_results.append(fold_res) + oof_frames.append(preds) + log.info(f"[outer {i}] E={E} held-out AUROC {fold_res['test_auroc']:.4f}") + + # ------------------------------------------------------------------ summary + oof = pd.concat(oof_frames, ignore_index=True) if oof_frames else pd.DataFrame() + oof.to_csv(base_out / "oof_predictions.csv", index=False) + aucs = [r["test_auroc"] for r in fold_results if r["test_auroc"] == r["test_auroc"]] + summary: Dict[str, Any] = { + "regime": regime, + "task": label_column, + "n_outer": len(fold_results), + "n_outer_total": len(folds), + "complete": len(fold_results) == len(folds), + "inner_k": inner_k, + "inner_seed": int(ncfg.inner_seed), + "epoch_reduce": str(ncfg.epoch_reduce), + "selection_metric": str(ncfg.selection_metric), + "max_epochs": int(cfg.trainer.max_epochs), + "probe": needs_probe, + "encoder_ckpt": cfg.model.get("init_from_ckpt"), + "auc_mean": float(np.mean(aucs)) if aucs else float("nan"), + "auc_sd": float(np.std(aucs, ddof=1)) if len(aucs) > 1 else 0.0, + "auc_folds": [round(a, 4) for a in aucs], + "folds": fold_results, + } + if not oof.empty and "prob_class_1" in oof.columns: + summary["pooled_region_auroc"] = _auroc(oof["label"], oof["prob_class_1"]) + agg = str(ncfg.get("patient_aggregation", "mean")).lower() + if agg != "mean": + raise ValueError( + "nested_cv.patient_aggregation supports 'mean' here; use grass-mil-predict for merged bags." + ) + pat = ( + oof.groupby("patient_id") + .agg( + prob_class_1=("prob_class_1", "mean"), + label=("label", "max"), + n_regions=("region_id", "nunique"), + outer=("outer", "first"), + ) + .reset_index() + ) + pat.to_csv(base_out / "oof_patient_predictions.csv", index=False) + summary["patient_auroc"] = _auroc(pat["label"], pat["prob_class_1"]) + summary["n_patients"] = int(len(pat)) + (base_out / str(ncfg.summary_filename)).write_text(json.dumps(summary, indent=1, default=str)) + log.info( + f"== [{regime}] AUROC {summary['auc_mean']:.4f} +/- {summary['auc_sd']:.4f} over " + f"{len(aucs)} outer folds; patient-level {summary.get('patient_auroc', float('nan')):.4f}" + ) + return summary + + +@hydra.main(version_base="1.3", config_path="configs", config_name="nested_cv.yaml") +def main(cfg: DictConfig) -> Optional[float]: + extras(cfg) + summary = run_nested_cv(cfg) + return summary.get("auc_mean") + + +if __name__ == "__main__": + main() diff --git a/src/grass_mil/probe.py b/src/grass_mil/probe.py new file mode 100644 index 0000000..f257ff1 --- /dev/null +++ b/src/grass_mil/probe.py @@ -0,0 +1,133 @@ +"""``grass-mil-probe``: fit the linear probe head on a frozen encoder.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Dict, Optional + +import hydra +import lightning as L +import torch +from omegaconf import DictConfig + +from grass_mil._paths import setup_project_root + +setup_project_root() + +from grass_mil.models.training.probe import ( # noqa: E402 + attach_datamodule, + collect_frozen_embeddings, + fit_probe, + probe_sampler_config, + save_probe_head, +) +from grass_mil.utils import RankedLogger, extras # noqa: E402 + +log = RankedLogger(__name__, rank_zero_only=True) + + +def _resolve_device(name: str) -> torch.device: + name = str(name or "auto").lower() + if name == "auto": + return torch.device("cuda" if torch.cuda.is_available() else "cpu") + return torch.device(name) + + +def run_probe(cfg: DictConfig) -> Dict[str, Any]: + if cfg.get("seed"): + L.seed_everything(cfg.seed, workers=True) + pcfg = cfg.probe + + datamodule = hydra.utils.instantiate(cfg.data) + datamodule.prepare_data() + datamodule.setup("fit") + + model = hydra.utils.instantiate(cfg.model) + trainer = hydra.utils.instantiate(cfg.trainer, logger=False, enable_checkpointing=False) + attach_datamodule(model, datamodule, trainer) + model.setup("fit") + device = _resolve_device(pcfg.get("device", "auto")) + model.to(device) + model.eval() + for p in model.parameters(): + p.requires_grad_(False) + + sampler_cfg = probe_sampler_config( + datamodule.sampler_config, + roots_per_unit=pcfg.get("roots_per_unit"), + seed=int(pcfg.get("seed", 0)), + batch_size=int(pcfg.get("batch_size", 256)), + ) + target_columns = cfg.task.get("target_columns") + target_columns = list(target_columns) if target_columns else None + num_classes = int(cfg.model.graph_head.output_dim) + + log.info( + f"Collecting frozen embeddings: {pcfg.get('roots_per_unit')} roots/region, " + f"seed {pcfg.get('seed', 0)}, {len(datamodule.dataset_train)} training regions" + ) + train_emb = collect_frozen_embeddings( + model, + datamodule.dataset_train, + sampler_config=sampler_cfg, + target_columns=target_columns, + device=device, + num_workers=int(cfg.data.get("num_workers", 0)), + ) + eval_sets = {} + for split in list(pcfg.get("eval_splits") or []): + ds = getattr(datamodule, f"dataset_{split}", None) + if ds is None or len(ds) == 0: + log.warning(f"probe.eval_splits: split '{split}' is empty; skipped") + continue + eval_sets[str(split)] = collect_frozen_embeddings( + model, + ds, + sampler_config=sampler_cfg, + target_columns=target_columns, + device=device, + num_workers=int(cfg.data.get("num_workers", 0)), + ) + + class_weight = pcfg.get("class_weight") + class_weight = None if class_weight in (None, "null", "none", "None") else str(class_weight) + fit = fit_probe( + train_emb, + num_classes=num_classes, + C=float(pcfg.get("C", 1.0)), + class_weight=class_weight, + max_iter=int(pcfg.get("max_iter", 3000)), + eval_sets=eval_sets, + ) + fit.settings.update( + { + "roots_per_unit": pcfg.get("roots_per_unit"), + "seed": int(pcfg.get("seed", 0)), + "sampler_runtime": dict(sampler_cfg.runtime), + "init_from_ckpt": cfg.model.get("init_from_ckpt"), + "n_train_regions": len(datamodule.dataset_train), + } + ) + out = save_probe_head(fit, str(pcfg.output_path)) + summary = {"output_path": str(out), "metrics": fit.metrics, "settings": fit.settings} + summary_path = Path(str(out)).with_suffix(".json") + summary_path.write_text(json.dumps(summary, indent=1, default=str)) + log.info( + f"Probe head written to {out}: " + + ", ".join( + f"{k}={v:.4f}" if isinstance(v, float) else f"{k}={v}" for k, v in fit.metrics.items() + ) + ) + return summary + + +@hydra.main(version_base="1.3", config_path="configs", config_name="probe.yaml") +def main(cfg: DictConfig) -> Optional[float]: + extras(cfg) + summary = run_probe(cfg) + return summary["metrics"].get("val_region_auroc") + + +if __name__ == "__main__": + main() diff --git a/src/grass_mil/spatial_stats/__init__.py b/src/grass_mil/spatial_stats/__init__.py new file mode 100644 index 0000000..b846a76 --- /dev/null +++ b/src/grass_mil/spatial_stats/__init__.py @@ -0,0 +1,24 @@ +"""Classical spatial statistics on the cell graph, per region, compared between groups. + +Neighbourhood enrichment z-scores (label permutations on the Delaunay graph), +cellular neighbourhoods (k-NN window composition clustered with k-means), +cell-cell contact fractions and Ripley cross-K, each summarised per region, +averaged per patient and compared between classes with Mann-Whitney + BH. +""" + +from .cellular_neighbourhoods import cellular_neighbourhoods, window_composition +from .contacts import contact_fractions +from .cross_k import cross_k +from .nhood_enrichment import nhood_enrichment_zscores +from .tests import bh_adjust, mann_whitney_table, patient_mean + +__all__ = [ + "bh_adjust", + "cellular_neighbourhoods", + "contact_fractions", + "cross_k", + "mann_whitney_table", + "nhood_enrichment_zscores", + "patient_mean", + "window_composition", +] diff --git a/src/grass_mil/spatial_stats/cellular_neighbourhoods.py b/src/grass_mil/spatial_stats/cellular_neighbourhoods.py new file mode 100644 index 0000000..7f8cb7c --- /dev/null +++ b/src/grass_mil/spatial_stats/cellular_neighbourhoods.py @@ -0,0 +1,51 @@ +"""Cellular neighbourhoods: k-NN window composition clustered with k-means.""" + +from __future__ import annotations + +from typing import List, Sequence, Tuple + +import numpy as np + +__all__ = ["window_composition", "cellular_neighbourhoods"] + + +def window_composition( + coords: np.ndarray, labels: np.ndarray, *, n_groups: int, k: int = 10 +) -> np.ndarray: + """Fraction of each group among a cell and its ``k`` nearest neighbours.""" + from scipy.spatial import cKDTree + + coords = np.asarray(coords, dtype=float) + labels = np.asarray(labels).astype(int) + n = coords.shape[0] + kk = min(int(k) + 1, n) + _, nn = cKDTree(coords).query(coords, k=kk) + nn = np.asarray(nn).reshape(n, -1) + W = np.zeros((n, n_groups), dtype=np.float32) + for j in range(nn.shape[1]): + W[np.arange(n), labels[nn[:, j]]] += 1 + return W / W.sum(axis=1, keepdims=True) + + +def cellular_neighbourhoods( + windows: Sequence[np.ndarray], + *, + n_cn: int = 10, + sample_size: int = 200_000, + seed: int = 0, + n_init: int = 10, + batch_size: int = 4096, +) -> Tuple[List[np.ndarray], np.ndarray]: + """Fit ``MiniBatchKMeans(n_cn)`` on a sample of all windows; return per-region CN labels and centres.""" + from sklearn.cluster import MiniBatchKMeans + + W = np.concatenate(list(windows)) + rng = np.random.default_rng(int(seed)) + sub = rng.choice(len(W), min(int(sample_size), len(W)), replace=False) + km = MiniBatchKMeans( + int(n_cn), random_state=int(seed), n_init=int(n_init), batch_size=int(batch_size) + ).fit(W[sub]) + out = [] + for w in windows: + out.append(km.predict(np.asarray(w, dtype=np.float32))) + return out, km.cluster_centers_ diff --git a/src/grass_mil/spatial_stats/cli.py b/src/grass_mil/spatial_stats/cli.py new file mode 100644 index 0000000..d7ffe58 --- /dev/null +++ b/src/grass_mil/spatial_stats/cli.py @@ -0,0 +1,61 @@ +"""``grass-mil-spatial-stats``: classical spatial statistics on a cell cache.""" + +from __future__ import annotations + +from typing import Any, Dict + +import hydra +from omegaconf import DictConfig, OmegaConf + +from grass_mil._paths import setup_project_root + +setup_project_root() + +from grass_mil.inference.cell_cache import CellCache # noqa: E402 +from grass_mil.interpretability.metaclusters_cli import load_labels_and_patients # noqa: E402 +from grass_mil.interpretability.niches_cli import resolve_regions # noqa: E402 +from grass_mil.spatial_stats.run import run_spatial_stats # noqa: E402 +from grass_mil.utils import RankedLogger # noqa: E402 + +log = RankedLogger(__name__, rank_zero_only=True) + +__all__ = ["run", "main"] + + +def run(cfg: DictConfig) -> Dict[str, Any]: + cache = CellCache.open(str(cfg.cells_dir)) + labels_of, patient_of = load_labels_and_patients(cfg.labels) + regions = resolve_regions(cfg.get("regions") or "all", cache) + return run_spatial_stats( + cache, + labels_of=labels_of, + patient_of=patient_of, + out_dir=str(cfg.output_dir), + regions=regions, + group_map=OmegaConf.to_container(cfg.group_map, resolve=True) + if cfg.get("group_map") + else None, + reference=list(cfg.reference) if cfg.get("reference") else None, + partners=list(cfg.partners) if cfg.get("partners") else None, + infiltrating=list(cfg.infiltrating) if cfg.get("infiltrating") else (), + n_perms=int(cfg.nhood.n_perms), + nhood_backend=str(cfg.nhood.backend), + window_k=int(cfg.cn.window_k), + n_cn=int(cfg.cn.n_cn), + cn_sample_size=int(cfg.cn.sample_size), + radii=[float(r) for r in cfg.crossk.radii], + seed=int(cfg.seed), + min_per_group=int(cfg.min_per_group), + positive_name=str(cfg.positive_name), + analyses=list(cfg.analyses), + log=log, + ) + + +@hydra.main(version_base="1.3", config_path="../configs", config_name="spatial_stats.yaml") +def main(cfg: DictConfig) -> None: + run(cfg) + + +if __name__ == "__main__": + main() diff --git a/src/grass_mil/spatial_stats/contacts.py b/src/grass_mil/spatial_stats/contacts.py new file mode 100644 index 0000000..3ed0201 --- /dev/null +++ b/src/grass_mil/spatial_stats/contacts.py @@ -0,0 +1,56 @@ +"""Contact fractions on the cell graph.""" + +from __future__ import annotations + +from typing import Dict, Sequence + +import numpy as np + +__all__ = ["contact_fractions"] + + +def contact_fractions( + labels: np.ndarray, + edge_index: np.ndarray, + *, + group_names: Sequence[str], + reference: Sequence[str], + partners: Sequence[str], + infiltrating: Sequence[str] = (), + min_cells: int = 5, +) -> Dict[str, float]: + """Fraction of reference cells touching >= 1 partner cell, homotypic contact of + the reference group, and for each infiltrating group the fraction of its cells + whose neighbours are mostly reference cells.""" + labels = np.asarray(labels).astype(int) + ei = np.asarray(edge_index).astype(int) + names = list(group_names) + gl = np.asarray(names, dtype=object)[labels] + n = labels.size + src = np.concatenate([ei[0], ei[1]]) + dst = np.concatenate([ei[1], ei[0]]) + ref = np.isin(gl, list(reference)) + out: Dict[str, float] = {} + for x in partners: + has = np.zeros(n, dtype=bool) + has[src[gl[dst] == x]] = True + out[f"contact_{x}"] = float(has[ref].mean()) if ref.sum() else float("nan") + has = np.zeros(n, dtype=bool) + has[src[ref[dst]]] = True + out["homotypic"] = float(has[ref].mean()) if ref.sum() else float("nan") + n_ref_nb = np.zeros(n) + deg = np.zeros(n) + np.add.at(n_ref_nb, src, ref[dst].astype(float)) + np.add.at(deg, src, 1.0) + frac_ref = n_ref_nb / np.maximum(deg, 1.0) + out["reference_fraction"] = float(ref.mean()) + for x in infiltrating: + mk = gl == x + out[f"{x}_intra_reference"] = ( + float((frac_ref[mk] > 0.5).mean()) if mk.sum() >= int(min_cells) else float("nan") + ) + if infiltrating: + imm = np.isin(gl, list(infiltrating)) + out["infiltration"] = float((frac_ref[imm] > 0.5).mean()) if imm.sum() else float("nan") + out["infiltrating_fraction"] = float(imm.mean()) + return out diff --git a/src/grass_mil/spatial_stats/cross_k.py b/src/grass_mil/spatial_stats/cross_k.py new file mode 100644 index 0000000..168216c --- /dev/null +++ b/src/grass_mil/spatial_stats/cross_k.py @@ -0,0 +1,48 @@ +"""Ripley cross-K between a reference group and partner groups, normalised by pi r^2.""" + +from __future__ import annotations + +from typing import Dict, Sequence + +import numpy as np + +__all__ = ["cross_k"] + + +def cross_k( + coords: np.ndarray, + labels: np.ndarray, + *, + group_names: Sequence[str], + reference: Sequence[str], + partners: Sequence[str], + radii: Sequence[float] = (20.0, 50.0, 100.0), + min_cells: int = 5, +) -> Dict[str, float]: + """``K(r) = area * pairs_within_r / (n_ref * n_partner) / (pi r^2)``; 1 = CSR. + + Self pairs are removed when the partner group is the reference itself. + """ + from scipy.spatial import cKDTree + + coords = np.asarray(coords, dtype=float) + labels = np.asarray(labels).astype(int) + gl = np.asarray(list(group_names), dtype=object)[labels] + ref = np.isin(gl, list(reference)) + out: Dict[str, float] = {} + if ref.sum() < int(min_cells): + return out + area = float(np.prod(coords.max(axis=0) - coords.min(axis=0))) + tree_ref = cKDTree(coords[ref]) + for x in partners: + mk = ref if x == "reference" else (gl == x) + if mk.sum() < int(min_cells): + continue + tree_x = cKDTree(coords[mk]) + self_pairs = int(mk.sum()) if x == "reference" else 0 + for r in radii: + cnt = tree_ref.count_neighbors(tree_x, float(r)) - self_pairs + out[f"K_{x}_r{int(r) if float(r).is_integer() else r}"] = float( + area * cnt / (ref.sum() * mk.sum()) / (np.pi * float(r) ** 2) + ) + return out diff --git a/src/grass_mil/spatial_stats/nhood_enrichment.py b/src/grass_mil/spatial_stats/nhood_enrichment.py new file mode 100644 index 0000000..9066019 --- /dev/null +++ b/src/grass_mil/spatial_stats/nhood_enrichment.py @@ -0,0 +1,103 @@ +"""Neighbourhood enrichment z-scores on a cell graph (label permutation null).""" + +from __future__ import annotations + +from typing import Optional + +import numpy as np + +__all__ = ["nhood_enrichment_zscores", "pair_counts"] + + +def pair_counts(labels: np.ndarray, edge_index: np.ndarray, n_groups: int) -> np.ndarray: + """Symmetric (undirected) count of edges between group pairs.""" + la = labels[edge_index[0]] + lb = labels[edge_index[1]] + M = np.zeros((n_groups, n_groups), dtype=np.float64) + np.add.at(M, (la, lb), 1.0) + return M + M.T + + +def nhood_enrichment_zscores( + labels: np.ndarray, + edge_index: np.ndarray, + *, + n_groups: int, + n_perms: int = 200, + seed: int = 0, + backend: str = "numpy", + coords: Optional[np.ndarray] = None, +) -> np.ndarray: + """z-score of observed vs permuted group-pair edge counts (squidpy's statistic). + + ``edge_index`` holds each undirected edge once or twice; counts are + symmetrised either way. ``backend='squidpy'`` delegates to + ``squidpy.gr.nhood_enrichment`` on the same graph (optional dependency). + Groups absent from the region get NaN rows/columns. + """ + labels = np.asarray(labels).astype(int) + ei = np.asarray(edge_index).astype(int) + if ei.ndim != 2 or ei.shape[0] != 2: + raise ValueError("edge_index must have shape (2, n_edges)") + # one direction only, so symmetrisation counts every edge exactly once per orientation + a, b = ei + keep = a < b + if keep.sum() == 0 and ei.shape[1] > 0: + keep = np.ones(ei.shape[1], dtype=bool) + ei = np.stack([a[keep], b[keep]]) + if backend == "squidpy": + return _squidpy_zscores( + labels, ei, n_groups=n_groups, n_perms=n_perms, seed=seed, coords=coords + ) + present = np.unique(labels) + rng = np.random.default_rng(int(seed)) + observed = pair_counts(labels, ei, n_groups) + perms = np.zeros((int(n_perms), n_groups, n_groups)) + for i in range(int(n_perms)): + perms[i] = pair_counts(rng.permutation(labels), ei, n_groups) + mean = perms.mean(axis=0) + std = perms.std(axis=0) + with np.errstate(divide="ignore", invalid="ignore"): + z = (observed - mean) / std + z[~np.isfinite(z)] = 0.0 + absent = np.setdiff1d(np.arange(n_groups), present) + z[absent, :] = np.nan + z[:, absent] = np.nan + return z + + +def _squidpy_zscores(labels, ei, *, n_groups, n_perms, seed, coords): + import anndata as ad + import pandas as pd + import scipy.sparse as sp + import squidpy as sq + + n = labels.size + A = sp.csr_matrix((np.ones(ei.shape[1]), (ei[0], ei[1])), shape=(n, n)) + A = ((A + A.T) > 0).astype(np.float32) + adata = ad.AnnData(X=np.zeros((n, 1), np.float32)) + adata.obs["g"] = pd.Categorical(labels.astype(str)) + adata.obsm["spatial"] = ( + np.asarray(coords, dtype=float) if coords is not None else np.zeros((n, 2)) + ) + adata.obsp["spatial_connectivities"] = A + adata.uns["spatial_neighbors"] = { + "connectivities_key": "spatial_connectivities", + "distances_key": "spatial_distances", + "params": {"n_neighbors": 6, "radius": None, "transform": None}, + } + sq.gr.nhood_enrichment( + adata, + cluster_key="g", + n_perms=int(n_perms), + seed=int(seed), + show_progress_bar=False, + n_jobs=1, + ) + z = adata.uns["g_nhood_enrichment"]["zscore"] + cats = [int(c) for c in adata.obs["g"].cat.categories] + out = np.full((n_groups, n_groups), np.nan) + for x, cx in enumerate(cats): + for y, cy in enumerate(cats): + out[cx, cy] = z[x, y] + return out diff --git a/src/grass_mil/spatial_stats/run.py b/src/grass_mil/spatial_stats/run.py new file mode 100644 index 0000000..90b7ba3 --- /dev/null +++ b/src/grass_mil/spatial_stats/run.py @@ -0,0 +1,224 @@ +"""Run the classical spatial statistics over a cell cache and compare groups.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Dict, List, Optional, Sequence + +import numpy as np +import pandas as pd + +from grass_mil.inference.cell_cache import CellCache + +from .cellular_neighbourhoods import cellular_neighbourhoods, window_composition +from .contacts import contact_fractions +from .cross_k import cross_k +from .nhood_enrichment import nhood_enrichment_zscores +from .tests import clr, mann_whitney_table, patient_mean + +__all__ = ["run_spatial_stats"] + + +def _resolve_groups(cell_type_names: Sequence[str], group_map: Optional[Dict[str, str]]): + """Fine cell types -> coarse groups (identity by default).""" + names = list(cell_type_names) + if not group_map: + groups = sorted(set(names)) + else: + groups = sorted({group_map.get(n, n) for n in names}) + gi = {g: i for i, g in enumerate(groups)} + fine_to_group = np.array([gi[(group_map or {}).get(n, n)] for n in names], dtype=int) + return groups, fine_to_group + + +def run_spatial_stats( + cache: CellCache, + *, + labels_of: Dict[str, int], + patient_of: Optional[Dict[str, str]] = None, + out_dir: str | Path, + regions: Optional[Sequence[str]] = None, + group_map: Optional[Dict[str, str]] = None, + reference: Optional[Sequence[str]] = None, + partners: Optional[Sequence[str]] = None, + infiltrating: Sequence[str] = (), + n_perms: int = 200, + nhood_backend: str = "numpy", + window_k: int = 10, + n_cn: int = 10, + cn_sample_size: int = 200_000, + radii: Sequence[float] = (20.0, 50.0, 100.0), + seed: int = 0, + min_per_group: int = 10, + positive_name: str = "positive", + analyses: Sequence[str] = ("nhood", "cn", "contacts", "crossk"), + log=None, +) -> Dict[str, Any]: + out = Path(out_dir) + out.mkdir(parents=True, exist_ok=True) + regions = sorted(str(r) for r in (regions if regions is not None else cache.regions)) + regions = [r for r in regions if r in labels_of] + names = cache.cell_type_names or [] + groups, fine_to_group = _resolve_groups(names, group_map) + G = len(groups) + pat_of = patient_of or {r: r for r in regions} + y_pat = pd.Series({pat_of.get(r, r): int(labels_of[r]) for r in regions}) + reference = list(reference) if reference else [] + partners = list(partners) if partners else [g for g in groups if g not in reference] + analyses = [a.lower() for a in analyses] + + Z: Dict[str, np.ndarray] = {} + windows: List[np.ndarray] = [] + contacts: List[Dict[str, Any]] = [] + crossk: List[Dict[str, Any]] = [] + for i, r in enumerate(regions): + d = cache.load(r, keys=["ct", "pos", "edge_index"]) + g = fine_to_group[d["ct"].astype(int)] + pos = d["pos"].astype(float) + ei = d["edge_index"].astype(int) + if "nhood" in analyses: + Z[r] = nhood_enrichment_zscores( + g, ei, n_groups=G, n_perms=n_perms, seed=seed, backend=nhood_backend, coords=pos + ) + if "cn" in analyses: + windows.append(window_composition(pos, g, n_groups=G, k=window_k)) + if "contacts" in analyses and reference: + rec = {"region_id": r} + rec.update( + contact_fractions( + g, + ei, + group_names=groups, + reference=reference, + partners=partners, + infiltrating=infiltrating, + ) + ) + contacts.append(rec) + if "crossk" in analyses and reference: + rec = {"region_id": r} + rec.update( + cross_k( + pos, + g, + group_names=groups, + reference=reference, + partners=partners + ["reference"], + radii=radii, + ) + ) + crossk.append(rec) + if log is not None and (i + 1) % 50 == 0: + log.info(f" {i + 1}/{len(regions)} regions") + + md = [ + f"# Classical spatial statistics ({len(regions)} regions, {len(y_pat)} patients; per region -> patient mean; Mann-Whitney {positive_name} vs rest, BH within analysis)", + "", + ] + results: Dict[str, Any] = {"regions": len(regions), "groups": groups} + if "nhood" in analyses and Z: + ZT = np.stack([Z[r] for r in regions]) + rows = {} + for a in range(G): + for b in range(a, G): + rows[f"{groups[a]}|{groups[b]}"] = ZT[:, a, b] + per_region = pd.DataFrame(rows, index=regions) + per_region.to_csv(out / "nhood_zscores_per_region.csv") + T = mann_whitney_table( + patient_mean(per_region, pat_of), y_pat, min_per_group=min_per_group + ) + T[["a", "b"]] = T["metric"].str.split("|", expand=True) + T.to_csv(out / "nhood_tests.csv", index=False) + results["nhood_tests"] = str(out / "nhood_tests.csv") + md += [ + f"## Neighbourhood enrichment (z-scores on the cell graph, {n_perms} label permutations, {nhood_backend}): pairs with q < 0.1 (top 20 by p otherwise)", + "", + "| Pair | z pos | z neg | diff | p | q |", + "|---|---|---|---|---|---|", + ] + sel = T[T.q < 0.1] if (T.q < 0.1).sum() else T.head(20) + for _, r in sel.iterrows(): + md.append( + f"| {r.a}-{r.b} | {r.mean_pos:+.2f} | {r.mean_neg:+.2f} | {r['diff']:+.2f} | {r.p:.3g} | {r.q:.2g} |" + ) + if "cn" in analyses and windows: + cn_labels, centres = cellular_neighbourhoods( + windows, n_cn=n_cn, sample_size=cn_sample_size, seed=seed + ) + CN = pd.DataFrame( + {f"CN{k}": [float(np.mean(lab == k)) for lab in cn_labels] for k in range(n_cn)}, + index=regions, + ) + CN.to_csv(out / "cn_shares_per_region.csv") + CNp = patient_mean(CN, pat_of) + L = pd.DataFrame(clr(CNp.to_numpy()), index=CNp.index, columns=CNp.columns) + T = mann_whitney_table(L, y_pat, min_per_group=min_per_group).rename( + columns={"mean_pos": "clr_pos", "mean_neg": "clr_neg", "diff": "clr_diff"} + ) + share_pos = CNp[y_pat.reindex(CNp.index) == 1].mean() + share_neg = CNp[y_pat.reindex(CNp.index) == 0].mean() + T["share_pos"] = T["metric"].map(share_pos) + T["share_neg"] = T["metric"].map(share_neg) + T["composition"] = T["metric"].map( + { + f"CN{k}": ", ".join( + f"{groups[j]} {100 * centres[k][j]:.0f}%" for j in np.argsort(-centres[k])[:3] + ) + for k in range(n_cn) + } + ) + T.to_csv(out / "cn_tests.csv", index=False) + pd.DataFrame(centres, columns=groups, index=[f"CN{k}" for k in range(n_cn)]).to_csv( + out / "cn_centres.csv" + ) + results["cn_tests"] = str(out / "cn_tests.csv") + md += [ + "", + f"## Cellular neighbourhoods ({window_k}-NN windows, k-means k={n_cn}), patient-level CLR shares", + "", + "| CN | composition | share pos | share neg | CLR diff | p | q |", + "|---|---|---|---|---|---|---|", + ] + for _, r in T.iterrows(): + md.append( + f"| {r.metric} | {r.composition} | {100 * r.share_pos:.1f}% | {100 * r.share_neg:.1f}% | {r.clr_diff:+.2f} | {r.p:.3g} | {r.q:.2g} |" + ) + if contacts: + C = pd.DataFrame(contacts).set_index("region_id") + C.to_csv(out / "contacts_per_region.csv") + T = mann_whitney_table(patient_mean(C, pat_of), y_pat, min_per_group=min_per_group) + T.to_csv(out / "contact_tests.csv", index=False) + results["contact_tests"] = str(out / "contact_tests.csv") + md += [ + "", + f"## Contact fractions (reference = {', '.join(reference)})", + "", + "| Metric | pos | neg | p | q |", + "|---|---|---|---|---|", + ] + for _, r in T.iterrows(): + md.append( + f"| {r.metric} | {r.mean_pos:.3f} | {r.mean_neg:.3f} | {r.p:.3g} | {r.q:.2g} |" + ) + if crossk: + X = pd.DataFrame(crossk).set_index("region_id") + X.to_csv(out / "crossk_per_region.csv") + T = mann_whitney_table(patient_mean(X, pat_of), y_pat, min_per_group=min_per_group) + T.to_csv(out / "crossk_tests.csv", index=False) + results["crossk_tests"] = str(out / "crossk_tests.csv") + md += [ + "", + f"## Cross-K (Ripley) reference -> partner, normalised by pi r^2 (1 = complete spatial randomness); radii {list(radii)}", + "", + "| Metric | pos | neg | p | q |", + "|---|---|---|---|---|", + ] + for _, r in T.iterrows(): + md.append( + f"| {r.metric} | {r.mean_pos:.2f} | {r.mean_neg:.2f} | {r.p:.3g} | {r.q:.2g} |" + ) + (out / "report_spatial.md").write_text("\n".join(md) + "\n") + results["report"] = str(out / "report_spatial.md") + (out / "spatial_stats_summary.json").write_text(json.dumps(results, indent=1, default=str)) + return results diff --git a/src/grass_mil/spatial_stats/tests.py b/src/grass_mil/spatial_stats/tests.py new file mode 100644 index 0000000..c327dcd --- /dev/null +++ b/src/grass_mil/spatial_stats/tests.py @@ -0,0 +1,64 @@ +"""Per-region -> per-patient summaries and two-group tests with BH correction.""" + +from __future__ import annotations + +from typing import Dict + +import numpy as np +import pandas as pd + +__all__ = ["bh_adjust", "mann_whitney_table", "patient_mean", "clr"] + + +def bh_adjust(p) -> np.ndarray: + from scipy.stats import false_discovery_control + + p = np.asarray(p, dtype=float) + q = np.full(p.size, np.nan) + ok = ~np.isnan(p) + if ok.sum(): + q[ok] = false_discovery_control(p[ok], method="bh") + return q + + +def patient_mean(per_region: pd.DataFrame, patient_of: Dict[str, str]) -> pd.DataFrame: + """Mean of every column per patient (index = region id).""" + df = per_region.copy() + df["__patient"] = [patient_of.get(str(r), str(r)) for r in df.index] + return df.groupby("__patient").mean(numeric_only=True) + + +def clr(M: np.ndarray, pseudo: float = 1e-4) -> np.ndarray: + L = np.log(np.asarray(M, dtype=float) + pseudo) + return L - L.mean(axis=1, keepdims=True) + + +def mann_whitney_table( + values: pd.DataFrame, y: pd.Series, *, min_per_group: int = 3 +) -> pd.DataFrame: + """Mann-Whitney U (two-sided) per column between y == 1 and y == 0, BH-adjusted.""" + from scipy import stats + + yy = y.reindex(values.index) + rows = [] + for col in values.columns: + v = values[col] + a = v[yy == 1].dropna() + b = v[yy == 0].dropna() + ok = len(a) >= int(min_per_group) and len(b) >= int(min_per_group) + rows.append( + dict( + metric=str(col), + n_pos=int(len(a)), + n_neg=int(len(b)), + mean_pos=float(a.mean()) if len(a) else float("nan"), + mean_neg=float(b.mean()) if len(b) else float("nan"), + diff=float(a.mean() - b.mean()) if len(a) and len(b) else float("nan"), + p=float(stats.mannwhitneyu(a, b, alternative="two-sided").pvalue) + if ok + else float("nan"), + ) + ) + T = pd.DataFrame(rows) + T["q"] = bh_adjust(T["p"].to_numpy()) + return T.sort_values("p").reset_index(drop=True) diff --git a/src/grass_mil/train.py b/src/grass_mil/train.py index 1f1eaf7..502b1aa 100644 --- a/src/grass_mil/train.py +++ b/src/grass_mil/train.py @@ -47,6 +47,13 @@ def _filter_val_monitor_callbacks( if not callbacks_cfg or not isinstance(callbacks_cfg, DictConfig): return callbacks_cfg, [] + # Resolve interpolations (``${paths.output_dir}`` and friends) first: the + # filtered copy below is a new root, so unresolved references into the + # full config would fail at instantiation. + try: + callbacks_cfg = OmegaConf.create(OmegaConf.to_container(callbacks_cfg, resolve=True)) + except Exception: # pragma: no cover - unresolvable outside a Hydra run + pass filtered = OmegaConf.create({}) removed: List[str] = [] for name, cb_conf in callbacks_cfg.items(): @@ -82,6 +89,11 @@ def train(cfg: DictConfig) -> Tuple[Dict[str, Any], Dict[str, Any]]: f"{removed}" ) callbacks: List[Callback] = instantiate_callbacks(callbacks_cfg) + output_dir = cfg.get("paths", {}).get("output_dir") if cfg.get("paths") else None + if output_dir and not _requires_zero_validation(cfg): + from grass_mil.callbacks import EpochMetricsWriter + + callbacks.append(EpochMetricsWriter(str(output_dir))) log.info("Instantiating loggers...") logger: List[Logger] = instantiate_loggers(cfg.get("logger")) @@ -127,6 +139,12 @@ def train(cfg: DictConfig) -> Tuple[Dict[str, Any], Dict[str, Any]]: ) trainer.test(model=model, datamodule=datamodule, ckpt_path=ckpt_path) log.info(f"Best ckpt path: {ckpt_path}") + if output_dir: + from grass_mil.callbacks import write_test_predictions + + written = write_test_predictions(model, str(output_dir)) + if written is not None: + log.info(f"Test predictions written to {written}") test_metrics = trainer.callback_metrics diff --git a/src/grass_mil/utils/rich_utils.py b/src/grass_mil/utils/rich_utils.py index ab2d158..778b53d 100644 --- a/src/grass_mil/utils/rich_utils.py +++ b/src/grass_mil/utils/rich_utils.py @@ -70,6 +70,7 @@ def print_config_tree( # save config tree to file if save_to_file: + Path(cfg.paths.output_dir).mkdir(parents=True, exist_ok=True) with open(Path(cfg.paths.output_dir, "config_tree.log"), "w") as file: rich.print(tree, file=file) @@ -95,5 +96,6 @@ def enforce_tags(cfg: DictConfig, save_to_file: bool = False) -> None: log.info(f"Tags: {cfg.tags}") if save_to_file: + Path(cfg.paths.output_dir).mkdir(parents=True, exist_ok=True) with open(Path(cfg.paths.output_dir, "tags.log"), "w") as file: rich.print(cfg.tags, file=file) diff --git a/tests/configs/test_component_instantiation.py b/tests/configs/test_component_instantiation.py index d8682b6..bd85d10 100644 --- a/tests/configs/test_component_instantiation.py +++ b/tests/configs/test_component_instantiation.py @@ -1,53 +1,84 @@ -from pathlib import Path +"""Compose the model/encoder and model/attention groups through Hydra and build them. -from tests.helpers.config_paths import CONFIGS_DIR +The groups ship as plain parameter dictionaries (no ``_target_``) that the +module factories turn into components, so the only faithful test is to compose +them the way the CLI does and instantiate the resulting module. +""" -import hydra +import pytest +from hydra import compose, initialize_config_module +from hydra.core.global_hydra import GlobalHydra from omegaconf import OmegaConf +from tests.helpers.config_paths import CONFIGS_DIR + +CONFIG_MODULE = "grass_mil.configs" +ENCODERS = ["gin", "gcn", "gat", "graphsage", "gine", "gine_length"] +ATTENTIONS = ["gated", "gated_projected"] + + +def _compose(overrides: list[str]): + GlobalHydra.instance().clear() + with initialize_config_module(version_base="1.3", config_module=CONFIG_MODULE): + cfg = compose(config_name="train", overrides=overrides) + GlobalHydra.instance().clear() + return cfg + + +@pytest.mark.parametrize("encoder", ENCODERS) +def test_encoder_group_builds(encoder: str) -> None: + from grass_mil.models.components.backbones import EncoderConfig, GNNEncoder + + cfg = _compose([f"model/encoder={encoder}", "model.encoder.input_dim=8"]) + enc_cfg = OmegaConf.to_container(cfg.model.encoder, resolve=True) + if enc_cfg.get("categorical_embedding") is not None: + enc_cfg["categorical_embedding"] = { + **enc_cfg["categorical_embedding"], + "num_embeddings": 5, + "column_index": 0, + } + encoder_module = GNNEncoder(EncoderConfig(**enc_cfg)) + assert encoder_module.output_dim == int(cfg.model.encoder.out_dim) -def _instantiate_yaml(path: Path): - cfg = OmegaConf.load(path) - return hydra.utils.instantiate(cfg) - - -def test_model_component_yaml_instantiation(): - base = CONFIGS_DIR / "model" - targets = [ - base / "encoder" / "gin.yaml", - base / "encoder" / "gcn.yaml", - base / "encoder" / "gat.yaml", - base / "encoder" / "graphsage.yaml", - base / "encoder" / "gine.yaml", - base / "attention" / "gated.yaml", - base / "attention" / "gated_projected.yaml", - base / "heads" / "graph.yaml", - base / "loss" / "categorical_ce.yaml", - base / "loss" / "categorical_bce.yaml", - base / "loss" / "regression_mse.yaml", - base / "loss" / "regression_huber.yaml", - base / "loss" / "survival_coxsgd.yaml", - ] - for path in targets: - obj = _instantiate_yaml(path) - assert obj is not None - - -def test_training_module_yaml_instantiation(): - base = CONFIGS_DIR / "model" - cases = [ - ("bgrl_module.yaml", "pretrain_bgrl.yaml", "cosine_step.yaml"), - ("supervised_module.yaml", "finetune_mean.yaml", "cosine_epoch.yaml"), - ("supervised_module.yaml", "finetune_mil.yaml", "cosine_epoch.yaml"), - ] - for model_name, task_name, scheduler_name in cases: - path = base / model_name - cfg = OmegaConf.load(path) - cfg.optim = OmegaConf.load(CONFIGS_DIR / "optim" / "adamw.yaml") - cfg.scheduler = OmegaConf.load(CONFIGS_DIR / "scheduler" / scheduler_name) - cfg.task = OmegaConf.load(CONFIGS_DIR / "task" / task_name).task - obj = hydra.utils.instantiate(cfg) - assert obj is not None + +@pytest.mark.parametrize("attention", ATTENTIONS) +def test_attention_group_builds(attention: str) -> None: + from grass_mil.models.components.factory import build_attention + + cfg = _compose([f"model/attention={attention}", "task=finetune_mil"]) + attn_cfg = OmegaConf.to_container(cfg.model.attention, resolve=True) + module = build_attention(use_attention=True, **attn_cfg) + assert module is not None + assert module.attention_c.out_features == int(cfg.model.graph_head.output_dim) + + +@pytest.mark.parametrize( + ("model_name", "task_name"), + [ + ("bgrl_module", "pretrain_bgrl"), + ("supervised_module", "finetune_mean"), + ("supervised_module", "finetune_mil"), + ("grass_mil", "finetune_mil"), + ], +) +def test_training_module_presets_instantiate(model_name: str, task_name: str) -> None: + import hydra + + cfg = _compose([f"model={model_name}", f"task={task_name}"]) + module = hydra.utils.instantiate(cfg.model) + assert module is not None + + +def test_grass_mil_preset_defaults_match_preprint() -> None: + cfg = _compose(["model=grass_mil", "task=finetune_mil"]) + enc = cfg.model.encoder + assert enc.conv_type == "gine" and int(enc.num_layers) == 4 and int(enc.hidden_dim) == 256 + assert enc.norm == "layernorm" and enc.pooling == "max" and bool(enc.train_eps) + assert int(enc.edge_feature_index) == 0 and int(enc.edge_attr_dim) == 1 + att = cfg.model.attention + assert int(att.projection_dim) == 32 and int(att.hidden_dim) == 8 and bool(att.dropout) + assert int(att.n_classes) == 2 and int(cfg.model.graph_head.output_dim) == 2 + assert int(cfg.model.graph_head.num_layers) == 2 def test_sampler_strategy_resolution(): @@ -60,11 +91,8 @@ def test_sampler_strategy_resolution(): assert strategy is not None -def test_default_component_and_data_sampler_configs(): - model_cfg = OmegaConf.load(CONFIGS_DIR / "model" / "components.yaml") - assert model_cfg.model.attention._target_ == ( - "grass_mil.models.components.attention.AttnNetGatedProjected" - ) - - data_cfg = OmegaConf.load(CONFIGS_DIR / "data" / "spatial_omics.yaml") - assert data_cfg.sampler.name == "shadow_custom" +def test_default_data_sampler_config(): + cfg = _compose([]) + assert cfg.data.sampler.name == "shadow_custom" + cfg_mil = _compose(["data/sampler=ego_graph_mil"]) + assert cfg_mil.data.sampler.runtime.unit_as_bag is True diff --git a/tests/contracts/test_model_component_contracts.py b/tests/contracts/test_model_component_contracts.py index 3a7ef4f..329ce93 100644 --- a/tests/contracts/test_model_component_contracts.py +++ b/tests/contracts/test_model_component_contracts.py @@ -8,12 +8,3 @@ def test_contract_markdown_exists_and_has_required_sections(): assert "Graph Unit Contract" in text assert "Loss Contracts" in text assert "Sampler Contracts" in text - - -def test_cursor_rule_exists_and_mentions_data_layer_agnosticity(): - path = Path(".cursor/rules/model_component_contracts.mdc") - assert path.exists() - text = path.read_text().lower() - assert "data-layer agnostic" in text - assert "native pyg" in text - assert "use custom sampler only" in text diff --git a/tests/eval/test_embeddings.py b/tests/eval/test_embeddings.py index 0c4838d..a4382ec 100644 --- a/tests/eval/test_embeddings.py +++ b/tests/eval/test_embeddings.py @@ -2,7 +2,7 @@ import torch -from grass_mil.inference.embeddings import embeddings_to_dataframe, node_embeddings_to_dataframe +from grass_mil.inference.io import embeddings_to_dataframe, node_embeddings_to_dataframe from grass_mil.inference.schemas import EmbeddingPayload diff --git a/tests/helpers/synthetic_cohort.py b/tests/helpers/synthetic_cohort.py new file mode 100644 index 0000000..3b3c025 --- /dev/null +++ b/tests/helpers/synthetic_cohort.py @@ -0,0 +1,176 @@ +"""A small synthetic cohort on disk, in the public data contract. + +Regions are CSV cell tables (``cell_id, x, y, cell_type, area``) listed in a +manifest; labels live in a CSV with a region id, a target and a patient column; +outer folds are a JSON list of region-id lists grouped by patient. Positive +regions carry a planted niche: cell type ``D`` forms a compact cluster next to +``C`` cells, while in negatives ``D`` is scattered. +""" + +from __future__ import annotations + +import json +import os +from dataclasses import dataclass, field +from pathlib import Path +from typing import Dict, List, Sequence + +import numpy as np +import pandas as pd +from hydra import compose, initialize_config_module +from hydra.core.global_hydra import GlobalHydra +from omegaconf import DictConfig, open_dict + +CELL_TYPES = ["A", "B", "C", "D"] + + +@dataclass +class SyntheticCohort: + root: Path + manifest_path: Path + label_path: Path + folds_path: Path + processed_dir: Path + region_ids: List[str] + patient_of: Dict[str, str] + label_of: Dict[str, int] + folds: List[List[str]] = field(default_factory=list) + + +def _region_cells(rng: np.random.Generator, n_cells: int, positive: bool, extent: float): + xy = rng.uniform(0.0, extent, size=(n_cells, 2)) + types = rng.choice(CELL_TYPES[:3], size=n_cells, p=[0.45, 0.35, 0.20]) + n_d = max(8, n_cells // 10) + d_idx = rng.choice(n_cells, size=n_d, replace=False) + types[d_idx] = "D" + if positive: + # Planted niche: D cells packed around a C cell. + c_idx = np.where(types == "C")[0] + centre = xy[rng.choice(c_idx)] if c_idx.size else xy[0] + xy[d_idx] = centre + rng.normal(0.0, extent * 0.04, size=(n_d, 2)) + xy = np.clip(xy, 0.0, extent) + area = rng.lognormal(mean=4.0, sigma=0.3, size=n_cells) + return pd.DataFrame( + { + "cell_id": [f"c{i}" for i in range(n_cells)], + "x": xy[:, 0], + "y": xy[:, 1], + "cell_type": types, + "area": area, + } + ) + + +def make_synthetic_cohort( + root: str | Path, + *, + n_patients: int = 6, + regions_per_patient: int = 2, + n_cells: int = 150, + n_outer_folds: int = 3, + extent: float = 600.0, + seed: int = 0, +) -> SyntheticCohort: + root = Path(root) + raw = root / "raw" + raw.mkdir(parents=True, exist_ok=True) + rng = np.random.default_rng(seed) + + rows = [] + label_rows = [] + region_ids: List[str] = [] + patient_of: Dict[str, str] = {} + label_of: Dict[str, int] = {} + for p in range(n_patients): + patient = f"P{p:02d}" + label = int(p % 2 == 1) + for r in range(regions_per_patient): + rid = f"{patient}_core{r}" + df = _region_cells(rng, n_cells, positive=bool(label), extent=extent) + path = raw / f"{rid}.csv" + df.to_csv(path, index=False) + rows.append( + {"sample_id": rid, "input_path": str(path), "input_type": "csv", "region_id": rid} + ) + label_rows.append({"REGION_ID": rid, "STAS": label, "PATIENT": patient}) + region_ids.append(rid) + patient_of[rid] = patient + label_of[rid] = label + manifest_path = raw / "manifest.csv" + pd.DataFrame(rows).to_csv(manifest_path, index=False) + label_path = raw / "labels.csv" + pd.DataFrame(label_rows).to_csv(label_path, index=False) + + # Patient-grouped outer folds: consecutive (negative, positive) patient pairs + # are dealt round-robin, so every fold holds both classes. + patients = sorted(set(patient_of.values())) + folds: List[List[str]] = [[] for _ in range(n_outer_folds)] + for i, patient in enumerate(patients): + folds[(i // 2) % n_outer_folds].extend(r for r in region_ids if patient_of[r] == patient) + folds_path = raw / "folds.json" + folds_path.write_text(json.dumps(folds)) + + return SyntheticCohort( + root=root, + manifest_path=manifest_path, + label_path=label_path, + folds_path=folds_path, + processed_dir=root / "processed", + region_ids=region_ids, + patient_of=patient_of, + label_of=label_of, + folds=folds, + ) + + +def cohort_overrides(cohort: SyntheticCohort, output_dir: str | Path) -> List[str]: + """Hydra overrides that point the public configs at the synthetic cohort.""" + return [ + "regime=mil", + f"paths.output_dir={output_dir}", + f"paths.root_dir={cohort.root}", + f"data.raw_manifest_path={cohort.manifest_path}", + f"data.processed_dir={cohort.processed_dir}", + "data.sample_unit=full", + "data.min_cells=5", + "data.csv.categorical_label_columns=[cell_type]", + "data.csv.molecular_columns=[area]", + "data.graph_builder.kwargs.edge_features=[log_distance,neighbor]", + "data.graph_builder.kwargs.neighbor_cutoff_um=55.0", + "data.split.split_by=region", + "data.split.train_val_test_split=[0.6,0.2,0.2]", + f"data.graph_labels.label_file={cohort.label_path}", + "data.graph_labels.id_column=REGION_ID", + "data.graph_labels.tasks=[STAS]", + "data.graph_labels.patient_column=PATIENT", + "data.sampler.runtime.depth=2", + "data.sampler.runtime.subsample_count=24", + "data.val_sampler.runtime.subsample_count=32", + "model.encoder.hidden_dim=16", + "model.encoder.out_dim=16", + "model.encoder.num_layers=2", + "model.attention.projection_dim=8", + "model.attention.hidden_dim=4", + "task.region_accumulation.hyperbatch_size=4", + "trainer.max_epochs=2", + "trainer.min_epochs=1", + "trainer.accelerator=cpu", + "trainer.devices=1", + "trainer.precision=32", + "+trainer.enable_progress_bar=false", + "~callbacks.rich_progress_bar", + "extras.print_config=false", + "extras.enforce_tags=false", + f"seed={os.environ.get('GRASS_MIL_TEST_SEED', '0')}", + ] + + +def compose_cfg(config_name: str, overrides: Sequence[str]) -> DictConfig: + GlobalHydra.instance().clear() + with initialize_config_module(version_base="1.3", config_module="grass_mil.configs"): + cfg = compose(config_name=config_name, overrides=list(overrides)) + GlobalHydra.instance().clear() + if "trainer" in cfg: + with open_dict(cfg): + cfg.trainer.default_root_dir = cfg.paths.output_dir + return cfg diff --git a/tests/interpretability/test_fidelity_phase6.py b/tests/interpretability/test_fidelity_phase6.py new file mode 100644 index 0000000..c8fcea4 --- /dev/null +++ b/tests/interpretability/test_fidelity_phase6.py @@ -0,0 +1,292 @@ +"""Phase 6: niche discovery (cosine HDBSCAN, relattn margins, tables, co-localisation, pipeline).""" + +from __future__ import annotations + +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +from tests.helpers.synthetic_cohort import cohort_overrides, compose_cfg, make_synthetic_cohort + +pytest.importorskip("sklearn") + + +def _blobs(n_per=200, d=16, seed=0): + rng = np.random.default_rng(seed) + centres = rng.normal(size=(3, d)) * 6 + X = np.concatenate([c + rng.normal(size=(n_per, d)) for c in centres]).astype(np.float32) + y = np.repeat(np.arange(3), n_per) + return X, y + + +def test_cosine_hdbscan_recovers_blobs_and_assigns_unsampled_rows(): + from sklearn.metrics import adjusted_rand_score + + from grass_mil.interpretability.core.clustering import cosine_hdbscan, fit_niche_clustering + + X, y = _blobs() + model, labels = cosine_hdbscan(X, n_components=5, min_cluster_size=20, sample_size=150, seed=0) + assert model.sample_idx.size == 150 and model.n_clusters == 3 + assigned = labels >= 0 + assert assigned.mean() > 0.95 + assert adjusted_rand_score(y[assigned], labels[assigned]) > 0.95 + unsampled = np.setdiff1d(np.arange(len(X)), model.sample_idx) + assert adjusted_rand_score(y[unsampled], labels[unsampled]) > 0.95 + assert np.array_equal(model.predict(X[model.sample_idx]), model.labels_sample) + # any registry backend: k-means with the same sample/assign machinery + km, lab_km = fit_niche_clustering( + X, + reduction={"method": "pca", "n_components": 5}, + clustering={"method": "kmeans", "n_clusters": 3}, + sample_size=None, + ) + assert adjusted_rand_score(y, lab_km) > 0.95 and km.clustering_method == "kmeans" + assert km.params["clustering"]["n_clusters"] == 3 + + +def test_relattn_formula_and_region_sum_identity(): + from grass_mil.interpretability.niches.margins import relattn_margins + + rng = np.random.default_rng(0) + rid = np.array(["a"] * 30 + ["b"] * 50) + LG = rng.normal(size=(80, 2)) + AR = rng.normal(size=(80, 2)) + bias = np.array([0.3, -0.2]) + res = relattn_margins(LG, AR, bias, rid) + for r, n in (("a", 30), ("b", 50)): + m = rid == r + a = np.exp(AR[m] - AR[m].max(0)) + a /= a.sum(0, keepdims=True) + direct = (a[:, 1] * (LG[m, 1] - bias[1]) - a[:, 0] * (LG[m, 0] - bias[0])) * n + assert np.allclose(res.margin[m], direct, atol=1e-5) + assert np.allclose(res.attention[m].sum(0), 1.0, atol=1e-5) + L = (a * LG[m]).sum(0) # bag logits = sum A l + assert res.margin[m].sum() / n == pytest.approx( + (L[1] - bias[1]) - (L[0] - bias[0]), abs=1e-5 + ) + + +def test_niche_table_and_colocalisation(): + from grass_mil.interpretability.niches.colocalisation import cluster_colocalisation + from grass_mil.interpretability.niches.tables import niche_table, spearman_concordance + + rng = np.random.default_rng(0) + n = 600 + rid = np.repeat([f"r{i}" for i in range(6)], 100) + region_labels = {f"r{i}": i % 2 for i in range(6)} + labels = rng.integers(0, 3, n) + labels[:20] = -1 + ct = rng.integers(0, 4, n) + margin = rng.normal(size=n) + (labels == 2) * 1.0 + att = rng.uniform(size=n) + T = niche_table( + labels, + cell_types=ct, + region_ids=rid, + region_labels=region_labels, + margin=margin, + attention_pos=att, + cell_type_names=list("ABCD"), + n_boot=50, + ) + assert set(T.cluster) == {"C0", "C1", "C2", "Noise"} + for col in ( + "share", + "cores_present", + "emp_frac_pos", + "share_auroc", + "relattn", + "relattn_lo", + "relattn_hi", + "signed_share", + "sign_consistency", + "lift", + "comp_A", + ): + assert col in T.columns + assert T.share.sum() == pytest.approx(1.0) + assert ( + T.set_index("cluster").loc["C2", "relattn"] > T.set_index("cluster").loc["C0", "relattn"] + ) + conc = spearman_concordance(T) + assert conc["n_clusters"] == 3 and "spearman_emp_relattn" in conc + + edges = np.stack([rng.integers(0, n, 3000), rng.integers(0, n, 3000)]) + CO, clusters = cluster_colocalisation(labels, edges) + assert clusters == [0, 1, 2] and np.allclose(CO, CO.T) + assert np.abs(CO).max() < 0.5 # random edges: observed ~ expected + # segregated graph: edges only within cluster -> positive diagonal, negative off-diagonal + same = np.array( + [ + (i, j) + for i in range(n) + for j in rng.integers(0, n, 5) + if labels[i] == labels[j] and labels[i] >= 0 + ][:3000] + ).T + CO2, _ = cluster_colocalisation(labels, same) + assert np.all(np.diag(CO2) > 0) and (CO2[~np.eye(3, dtype=bool)] < 0).all() + + +def test_identity_residual_from_bag_table(): + from grass_mil.interpretability.core.attribution import niche_attribution_summary + from grass_mil.interpretability.plugins.builtin import _bag_logits_from_table + + rng = np.random.default_rng(0) + rows = [] + bag_rows = [] + for b in range(3): + n = 10 + lg = rng.normal(size=(n, 2)) + a_logit = rng.normal(size=(n, 2)) + a = np.exp(a_logit - a_logit.max(0)) + a /= a.sum(0) + L = (a * lg).sum(0) + bag_rows.append({"bag_id": f"b{b}", "logit_0": L[0], "logit_1": L[1]}) + for i in range(n): + rows.append( + { + "bag_id": f"b{b}", + "attention_c0": a[i, 0], + "attention_c1": a[i, 1], + "logit_0": lg[i, 0], + "logit_1": lg[i, 1], + } + ) + table = pd.DataFrame(rows) + bag_logits = _bag_logits_from_table( + pd.DataFrame(bag_rows), bag_id_column="bag_id", logit_prefix="logit_" + ) + assert set(bag_logits) == {"b0", "b1", "b2"} + res = niche_attribution_summary( + table, + np.zeros(len(table), dtype=int), + attention_columns=["attention_c0", "attention_c1"], + logit_columns=["logit_0", "logit_1"], + bag_logits=bag_logits, + n_bootstrap=10, + ) + assert res.identity_residual < 1e-6 + assert np.isnan( + niche_attribution_summary( + table, + np.zeros(len(table), dtype=int), + attention_columns=["attention_c0", "attention_c1"], + logit_columns=["logit_0", "logit_1"], + n_bootstrap=10, + ).identity_residual + ) + + +@pytest.fixture(scope="module") +def cache_dir(tmp_path_factory): + pytest.importorskip("torch_geometric") + import hydra + + from grass_mil.extract import run_extract + from grass_mil.train import train + + root = tmp_path_factory.mktemp("cohort6") + c = make_synthetic_cohort(root, n_patients=4, regions_per_patient=2, n_cells=80) + cfg = compose_cfg("train", cohort_overrides(c, root / "pre")) + hydra.utils.instantiate(cfg.data).prepare_data() + train_out = root / "train" + train(compose_cfg("train", cohort_overrides(c, train_out) + ["trainer.max_epochs=1"])) + ckpt = train_out / "checkpoints" / "last.ckpt" + ecfg = compose_cfg( + "extract", + cohort_overrides(c, root / "extract") + + [f"+extract.models.final={ckpt}", "extract.device=cpu"], + ) + result = run_extract(ecfg) + return Path(result["models"][0]), ckpt + + +def test_niche_discovery_pipeline_default_and_kmeans(cache_dir, tmp_path): + from grass_mil.inference.cell_cache import CellCache + from grass_mil.interpretability.niches.pipeline import run_niche_discovery, write_niche_outputs + + model_dir, ckpt = cache_dir + cache = CellCache.open(model_dir) + res = run_niche_discovery( + cache, + regions="all" and cache.regions, + clustering={ + "method": "hdbscan", + "params": { + "min_cluster_size": 15, + "min_samples": 1, + "cluster_selection_method": "eom", + }, + }, + sample_size=None, + n_boot=50, + ) + assert res.labels.size == sum(cache.n_cells(r) for r in cache.regions) + assert res.n_clusters >= 1 and res.table.share.sum() == pytest.approx(1.0) + assert res.colocalisation.shape == (res.n_clusters, res.n_clusters) + paths = write_niche_outputs(res, tmp_path / "default", title="test", umap={"enabled": False}) + for key in ( + "labels", + "cluster_model", + "cells", + "clusters", + "report", + "pca_triptych", + "clusters_overview", + "colocalisation", + ): + assert key in paths and Path(paths[key]).exists(), key + cells = pd.read_csv(paths["cells"]) + assert len(cells) == res.labels.size and set(cells.columns) >= { + "region_id", + "cluster", + "cell_type", + "relattn", + "x", + "y", + } + + km = run_niche_discovery( + cache, + regions=cache.regions, + clustering={"method": "kmeans", "n_clusters": 4}, + reduction={"method": "pca", "n_components": 5}, + sample_size=None, + n_boot=20, + ) + assert km.n_clusters == 4 and km.noise_fraction == 0.0 + + att = run_niche_discovery( + cache, + regions=cache.regions, + space="attention", + attention_ckpt=str(ckpt), + clustering={"method": "kmeans", "n_clusters": 3}, + reduction={"method": "none"}, + sample_size=None, + n_boot=20, + ) + assert att.features.shape[1] == 4 # attention hidden_dim of the synthetic model + + +def test_niches_cli_runs(cache_dir, tmp_path): + from grass_mil.interpretability.niches_cli import run_niches + + model_dir, _ = cache_dir + cfg = compose_cfg( + "niches", + [ + f"cells_dir={model_dir}", + f"output_dir={tmp_path / 'niches'}", + "regions=all", + "clustering.params.min_cluster_size=15", + "sample_size=null", + "n_boot=20", + "umap.enabled=false", + ], + ) + out = run_niches(cfg) + assert out["n_clusters"] >= 1 and Path(out["paths"]["report"]).exists() diff --git a/tests/interpretability/test_fidelity_phase7.py b/tests/interpretability/test_fidelity_phase7.py new file mode 100644 index 0000000..10d49a9 --- /dev/null +++ b/tests/interpretability/test_fidelity_phase7.py @@ -0,0 +1,176 @@ +"""Phase 7: cross-fold metaclusters, transfer and shared basis.""" + +from __future__ import annotations + +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +from tests.helpers.synthetic_cohort import cohort_overrides, compose_cfg, make_synthetic_cohort + +pytest.importorskip("sklearn") + + +def test_hellinger_range_and_constrained_linkage_never_merges_same_fold(): + from grass_mil.interpretability.metaclusters.match import ( + constrained_average_linkage, + hellinger_matrix, + scan_threshold, + ) + + comp = np.array([[1, 0, 0], [0, 1, 0], [0.5, 0.5, 0], [1, 0, 0], [0, 0, 1]], dtype=float) + D = hellinger_matrix(comp) + assert D.shape == (5, 5) and np.allclose(D, D.T) and D.min() >= 0 and D.max() <= 1 + 1e-9 + assert D[0, 3] == 0 and D[0, 1] == pytest.approx(1.0) + fold = np.array(["f0", "f1", "f0", "f1", "f2"]) + groups = constrained_average_linkage(D, fold, 0.9) + for g in groups: + assert len(set(fold[g])) == len(g) # cannot-link respected + assert sorted(sorted(g) for g in groups)[0] == [0, 3] # identical compositions merge + thr, best, scan = scan_threshold(D, fold, grid=[0.05, 0.5, 0.9], min_folds=2) + assert scan.n_replicated.max() == sum(len(g) >= 2 for g in best) + # ties -> fewest metaclusters + tied = scan[scan.n_replicated == scan.n_replicated.max()] + assert len(best) == tied.n_metaclusters.min() + + +def test_patient_shares_and_tests(): + from grass_mil.interpretability.metaclusters.match import patient_shares, patient_tests + + region_labels = { + "r0": np.array([0, 0, 1, -1]), + "r1": np.array([1, 1, 1, 1]), + "r2": np.array([0, 0, 0, 0]), + } + region_fold = {"r0": "f0", "r1": "f0", "r2": "f1"} + id_to_meta = {"f0_C0": 1, "f0_C1": 2, "f1_C0": 1} + reg, pat = patient_shares( + region_labels, region_fold, id_to_meta, {"r0": "P", "r1": "P", "r2": "Q"}, [1, 2] + ) + assert ( + reg.loc["r0", "M1"] == 0.5 and reg.loc["r0", "M2"] == 0.25 and reg.loc["r2", "M1"] == 1.0 + ) + assert (reg.sum(axis=1) <= 1 + 1e-9).all() and pat.loc["P", "M2"] == pytest.approx( + (0.25 + 1.0) / 2 + ) + shares = pd.DataFrame( + {"M1": np.r_[np.ones(5), np.zeros(5)], "M2": np.random.default_rng(0).uniform(size=10)}, + index=[f"p{i}" for i in range(10)], + ) + y = pd.Series(np.r_[np.ones(5), np.zeros(5)].astype(int), index=shares.index) + T = patient_tests(shares, y) + assert set(T.columns) >= {"meta", "share_pos", "share_neg", "auroc", "p", "q"} + assert T.set_index("meta").loc[1, "auroc"] == 1.0 + + +@pytest.fixture(scope="module") +def two_fold_caches(tmp_path_factory): + pytest.importorskip("torch_geometric") + import hydra + + from grass_mil.extract import run_extract + from grass_mil.train import train + + root = tmp_path_factory.mktemp("cohort7") + c = make_synthetic_cohort( + root, n_patients=6, regions_per_patient=2, n_cells=80, n_outer_folds=2 + ) + hydra.utils.instantiate( + compose_cfg("train", cohort_overrides(c, root / "pre")).data + ).prepare_data() + ckpts = {} + for i in range(2): + test_ids = c.folds[i] + train_ids = [r for j, f in enumerate(c.folds) if j != i for r in f] + out = root / f"train{i}" + train( + compose_cfg( + "train", + cohort_overrides(c, out) + + [ + "trainer.max_epochs=1", + f"data.split.explicit.train=[{','.join(train_ids)}]", + f"data.split.explicit.val=[{','.join(test_ids)}]", + f"data.split.explicit.test=[{','.join(test_ids)}]", + ], + ) + ) + ckpts[f"fold{i}"] = (out / "checkpoints" / "last.ckpt", test_ids) + overrides = cohort_overrides(c, root / "extract") + ["extract.device=cpu"] + for name, (ckpt, held) in ckpts.items(): + overrides += [ + f"+extract.models.{name}={ckpt}", + f"+extract.held_out.{name}=[{','.join(held)}]", + ] + result = run_extract(compose_cfg("extract", overrides)) + return c, Path(result["output_dir"]) + + +def test_metaclusters_cli_all_stages(two_fold_caches, tmp_path): + from grass_mil.interpretability.metaclusters_cli import run_metaclusters + + c, cells_dir = two_fold_caches + cfg = compose_cfg( + "metaclusters", + [ + f"cells_dir={cells_dir}", + f"output_dir={tmp_path / 'meta'}", + f"labels.label_file={c.label_path}", + "labels.id_column=REGION_ID", + "labels.label_column=STAS", + "labels.patient_column=PATIENT", + "clustering.method=kmeans", + "clustering.n_clusters=3", + "sample_size=null", + "n_boot=20", + "match.min_folds=2", + "transfer.min_cells=5", + "basis.k=4", + "basis.sample_size=1000", + "basis.n_init=2", + ], + ) + summary = run_metaclusters(cfg) + assert set(summary["cluster"]) == {"fold0", "fold1"} + out = tmp_path / "meta" + fc = pd.read_csv(out / "cluster" / "fold_clusters.csv") + assert set(fc.fold) == {"fold0", "fold1"} and len(fc) == 6 + mt = pd.read_csv(out / "match" / "metaclusters.csv") + assert mt.n_folds.max() <= 2 and summary["match"]["n_metaclusters"] == len(mt) + matched = pd.read_csv(out / "match" / "fold_clusters_matched.csv") + for _, g in matched.groupby("meta"): + assert g.fold.nunique() == len(g) # cannot-link + shares = pd.read_csv(out / "match" / "metacluster_patient_shares.csv", index_col=0) + assert len(shares) == 6 and (shares.sum(axis=1) <= 1 + 1e-6).all() + assert (out / "match" / "metacluster_patient_tests.csv").exists() + ML = np.load(out / "transfer" / "transfer_metalabels.npy") + assert ML.shape[0] == 2 and ML.shape[1] == sum(80 for _ in c.region_ids) + ari = pd.read_csv(out / "transfer" / "transfer_model_ari.csv", index_col=0) + assert ari.loc["fold0", "fold0"] == pytest.approx(1.0) + assert all(v > 0.99 for v in summary["transfer"]["refit_agreement"].values()) + basis = pd.read_csv(out / "basis" / "basis_attribution.csv") + assert len(basis) == 4 and {"relattn_heldout", "relattn_train", "relattn_all"} <= set( + basis.columns + ) + assert (out / "basis" / "report_basis.md").exists() and ( + out / "metaclusters_summary.json" + ).exists() + + # fixed threshold + stage re-run reuses stage-1 outputs + cfg2 = compose_cfg( + "metaclusters", + [ + f"cells_dir={cells_dir}", + f"output_dir={tmp_path / 'meta'}", + f"labels.label_file={c.label_path}", + "labels.label_column=STAS", + "labels.patient_column=PATIENT", + "stage=match", + "match.threshold=0.4", + "match.min_folds=2", + ], + ) + s2 = run_metaclusters(cfg2) + assert s2["match"]["threshold"] == 0.4 diff --git a/tests/interpretability/test_spatial_statistics.py b/tests/interpretability/test_spatial_statistics.py index dadf4c3..9b6d5e4 100644 --- a/tests/interpretability/test_spatial_statistics.py +++ b/tests/interpretability/test_spatial_statistics.py @@ -15,7 +15,6 @@ compute_niche_agreement, ) from grass_mil.interpretability.tier2.autocorrelation import ( - diff_morans_i_vs_reference, morans_i, run_morans_i, ) @@ -104,25 +103,6 @@ def test_run_morans_i_skips_groups_below_the_size_threshold() -> None: assert np.isnan(result.statistic.loc["small", "feature"]) -def test_diff_morans_i_reports_a_z_statistic_against_the_reference() -> None: - nodes, edges = _autocorrelation_tables() - nodes["niche_label"] = nodes["niche_label"].map({"a": 0, "b": -1}) - result = run_morans_i( - nodes, - edges, - feature_columns=["feature"], - group_column="niche_label", - n_perms=50, - random_state=0, - ) - diff = diff_morans_i_vs_reference(result, reference_group=-1) - assert list(diff.index) == [0] - assert diff.loc[0, "feature"] > 0 - - with pytest.raises(ValueError, match="not present"): - diff_morans_i_vs_reference(result, reference_group=99) - - def _poisson_points(n: int, seed: int, size: float = 100.0) -> np.ndarray: rng = np.random.default_rng(seed) return rng.uniform(0.0, size, size=(n, 2)) diff --git a/tests/models/test_fidelity_phase1.py b/tests/models/test_fidelity_phase1.py new file mode 100644 index 0000000..2d79975 --- /dev/null +++ b/tests/models/test_fidelity_phase1.py @@ -0,0 +1,114 @@ +"""Phase 1 fidelity: graph edge features, encoder epsilon/init, attention init and space.""" + +import numpy as np +import torch +from torch import nn + +from grass_mil.data.components.graph_builders import DelaunayGraphBuilder, log_distance_feature +from grass_mil.models.components.attention import AttnNetGatedProjected +from grass_mil.models.components.backbones import EncoderConfig, GNNEncoder +from grass_mil.models.components.embeddings import ( + CategoricalEmbeddingConfig, + NodeInputEmbedding, + resolve_categorical_embedding_config, +) +from grass_mil.models.components.factory import build_loss + + +def test_log_distance_matches_reference_formula(): + d = np.array([0.0, 7.389, 20.0, 148.41, 1000.0]) + got = log_distance_feature(d, log_lower=2.0, log_upper=5.0) + expected = np.clip((np.log(d + 1e-5) - 2.0) / 3.0, 0.0, 1.0) + assert np.allclose(got, expected, atol=1e-6) + assert got[0] == 0.0 and got[-1] == 1.0 + + +def test_delaunay_builder_emits_log_distance_and_stores_edges_twice(): + rng = np.random.default_rng(0) + coords = rng.uniform(0, 500, size=(60, 2)) + builder = DelaunayGraphBuilder( + edge_features=["log_distance", "neighbor", "distance"], neighbor_cutoff_um=55.0 + ) + res = builder.build(coords) + assert res.edge_attr_names == ["log_distance", "neighbor", "distance"] + ei = res.edge_index.numpy() + pairs = set(map(tuple, ei.T.tolist())) + assert all((b, a) in pairs for a, b in pairs) + dist = res.edge_attr[:, 2].numpy() + assert np.allclose(res.edge_attr[:, 0].numpy(), log_distance_feature(dist), atol=1e-6) + assert np.array_equal(res.edge_attr[:, 1].numpy(), (dist <= 55.0).astype(np.float32)) + + +def _gine_cfg(**kw): + base = dict( + input_dim=3, + hidden_dim=16, + out_dim=16, + num_layers=2, + conv_type="gine", + norm="layernorm", + pooling="max", + edge_attr_dim=1, + edge_feature_index=0, + use_edge_attr=True, + ) + base.update(kw) + return EncoderConfig(**base) + + +def test_gine_eps_is_trainable_by_default_and_fixed_when_disabled(): + enc = GNNEncoder(_gine_cfg()) + assert all(isinstance(layer.eps, nn.Parameter) for layer in enc.layers) + enc_fixed = GNNEncoder(_gine_cfg(train_eps=False)) + assert all(not isinstance(layer.eps, nn.Parameter) for layer in enc_fixed.layers) + + +def test_input_embedding_xavier_init_changes_scale_and_default_keeps_torch(): + torch.manual_seed(0) + cat = CategoricalEmbeddingConfig(label="cell_type", num_embeddings=10, column_index=0) + xav = NodeInputEmbedding(input_dim=4, hidden_dim=64, categorical=cat, init="xavier") + torch.manual_seed(0) + dflt = NodeInputEmbedding(input_dim=4, hidden_dim=64, categorical=cat, init="default") + # nn.Embedding default is N(0, 1); xavier bound for (11, 64) is sqrt(6 / 75) ~ 0.28. + assert xav.embedding.weight.abs().max() < 0.5 + assert dflt.embedding.weight.abs().max() > 1.0 + + +def test_unassigned_label_resolves_to_existing_category(): + cfg = {"label": "cell_type", "reserve_unassigned": False, "unassigned_label": "other"} + resolved = resolve_categorical_embedding_config( + cfg, + cardinalities={"cell_type": 3}, + slices={"cell_type": 0}, + label_maps={"cell_type": {"tumor": 0, "other": 1, "immune": 2}}, + ) + assert resolved.unassigned_index == 1 + emb = NodeInputEmbedding(input_dim=0, hidden_dim=8, categorical=resolved) + assert emb.unassigned_index == 1 + assert emb.embedding.num_embeddings == 3 + + +def test_attention_kaiming_init_and_attention_space(): + torch.manual_seed(0) + att = AttnNetGatedProjected( + input_dim=32, projection_dim=8, hidden_dim=4, dropout=True, n_classes=2 + ) + att.eval() + x = torch.randn(20, 32) + space = att.attention_space(x) + assert space.shape == (20, 4) + manual = att.attention_a[0](att.projection[1](att.projection[0](x))) + assert torch.allclose(space, manual) + logits, passthrough = att(x) + assert logits.shape == (20, 2) and passthrough is x + # kaiming_uniform (a=0): bound = sqrt(2) * sqrt(3 / fan_in) = sqrt(6 / 32) ~ 0.433 + assert att.projection[0].weight.abs().max() <= np.sqrt(6.0 / 32) + 1e-6 + assert att.projection[0].weight.abs().max() > 1.0 / np.sqrt(32) # wider than torch default + + +def test_loss_weights_accept_lists(): + loss = build_loss("categorical_ce", class_weight=[1.0, 3.0]) + assert isinstance(loss.class_weight, torch.Tensor) + logits = torch.randn(5, 2) + target = torch.tensor([0, 1, 1, 0, 1]) + assert torch.isfinite(loss(logits, target)) diff --git a/tests/models/test_fidelity_phase2.py b/tests/models/test_fidelity_phase2.py new file mode 100644 index 0000000..5e4d165 --- /dev/null +++ b/tests/models/test_fidelity_phase2.py @@ -0,0 +1,284 @@ +"""Phase 2 fidelity: region bags, root sampling, parameter groups, schedules, class weights, +fine-tune augmentation, regime presets and region-level metrics.""" + +from pathlib import Path + +import pytest +import torch +from hydra import compose, initialize_config_module +from hydra.core.global_hydra import GlobalHydra +from omegaconf import OmegaConf, open_dict + +from grass_mil.data.components.samplers import ( + UNLIMITED_NEIGHBORS, + RuntimeShadowConfig, + ShadowCustomStrategy, +) +from grass_mil.models.training import ( + build_finetune_augmentation, + build_param_groups, + drop_features, + inverse_frequency_class_weights, + resolve_t_max_auto, +) + +CONFIG_MODULE = "grass_mil.configs" + + +def _compose(overrides: list[str]): + GlobalHydra.instance().clear() + with initialize_config_module(version_base="1.3", config_module=CONFIG_MODULE): + cfg = compose(config_name="train", overrides=overrides) + GlobalHydra.instance().clear() + return cfg + + +def _units(n_units: int = 3, n_nodes: int = 40): + from torch_geometric.data import Data + + out = [] + g = torch.Generator().manual_seed(0) + for i in range(n_units): + edge_index = torch.randint(0, n_nodes, (2, 160), generator=g) + data = Data( + x=torch.randn(n_nodes, 4, generator=g), + edge_index=edge_index, + edge_attr=torch.rand(160, 2, generator=g), + graph_y=torch.tensor([[float(i % 2)]]), + graph_w=torch.ones(1, 1), + num_nodes=n_nodes, + ) + data.region_id = f"region_{i}" + data.sample_id = f"sample_{i}" + data.patch_id = f"patch_{i}" + out.append(data) + return out + + +def _runtime(**kw): + base = dict( + enabled=True, + depth=2, + num_neighbors=-1, + subgraph_batch_size=8, + proportional_root_sampling=False, + ) + base.update(kw) + return base + + +def test_num_neighbors_minus_one_means_unlimited(): + cfg = RuntimeShadowConfig.from_dict(_runtime()) + assert cfg.num_neighbors == UNLIMITED_NEIGHBORS + + +def test_unit_as_bag_yields_one_batch_per_unit_with_subsample_count_roots(): + strategy = ShadowCustomStrategy(runtime=_runtime(subsample_count=12, unit_as_bag=True)) + loader = strategy.build_dataset_loader(dataset=_units(), batch_size=1, shuffle=False) + batches = list(loader) + assert len(batches) == 3 == len(loader) + for batch in batches: + assert int(batch.ptr.numel()) - 1 == 12 + assert len(set(batch.region_id)) == 1 + + +def test_fixed_per_unit_seed_gives_identical_roots_every_pass(): + strategy = ShadowCustomStrategy( + runtime=_runtime(subsample_count=10, unit_as_bag=True, fixed_per_unit_seed=7) + ) + loader = strategy.build_dataset_loader(dataset=_units(), batch_size=1, shuffle=False) + first = [b.root_n_id.sort().values.tolist() for b in loader] + second = [b.root_n_id.sort().values.tolist() for b in loader] + assert first == second + unseeded = ShadowCustomStrategy(runtime=_runtime(subsample_count=10, unit_as_bag=True)) + loader2 = unseeded.build_dataset_loader(dataset=_units(), batch_size=1, shuffle=False) + a = [b.root_n_id.sort().values.tolist() for b in loader2] + b = [b.root_n_id.sort().values.tolist() for b in loader2] + assert a != b + + +def test_all_roots_mode_covers_every_node_once(): + strategy = ShadowCustomStrategy(runtime=_runtime(subsample_count=None, subgraph_batch_size=16)) + loader = strategy.build_dataset_loader( + dataset=_units(n_units=1, n_nodes=40), batch_size=1, shuffle=False + ) + roots = torch.cat([b.root_n_id for b in loader]).sort().values.tolist() + assert roots == list(range(40)) + + +def test_build_param_groups_scales_and_freezes(): + enc = torch.nn.Linear(4, 4) + head = torch.nn.Linear(4, 2) + att = torch.nn.Linear(4, 2) + groups = build_param_groups( + base_lr=1e-4, + base_weight_decay=1e-3, + encoder=enc, + head=head, + attention=att, + encoder_lr_scale=0.0, + head_lr_scale=0.01, + attention_lr_scale=2.0, + head_weight_decay=5e-4, + ) + names = [g["name"] for g in groups] + assert names == ["head", "attention"] + assert groups[0]["lr"] == pytest.approx(1e-6) and groups[0]["weight_decay"] == pytest.approx( + 5e-4 + ) + assert groups[1]["lr"] == pytest.approx(2e-4) and groups[1]["weight_decay"] == pytest.approx( + 1e-3 + ) + with pytest.raises(ValueError): + build_param_groups( + base_lr=1e-4, + base_weight_decay=0.0, + encoder=enc, + head=head, + attention=att, + encoder_lr_scale=0.0, + head_lr_scale=0.0, + attention_lr_scale=0.0, + ) + + +def test_t_max_auto_counts_optimizer_steps(): + assert resolve_t_max_auto(num_train_units=340, max_epochs=25, hyperbatch_size=12) == 25 * 29 + assert resolve_t_max_auto(num_train_units=5, max_epochs=3, hyperbatch_size=12) == 3 + + +def test_inverse_frequency_class_weights_have_mean_one(): + w = inverse_frequency_class_weights([0] * 70 + [1] * 30, 2) + assert w.mean().item() == pytest.approx(1.0) + assert w[1] > w[0] + assert (w[1] / w[0]).item() == pytest.approx(70 / 30) + + +def test_elementwise_feature_drop_writes_unassigned_token(): + from torch_geometric.data import Data + + data = Data( + x=torch.ones(200, 3), edge_index=torch.empty(2, 0, dtype=torch.long), num_nodes=200 + ) + data.categorical_codes = torch.zeros(200, 1, dtype=torch.long) + gen = torch.Generator().manual_seed(0) + out = drop_features( + data, p=0.3, generator=gen, mode="elementwise", categorical_column=0, unassigned_index=9 + ) + zero_frac = float((out.x == 0).float().mean()) + assert 0.2 < zero_frac < 0.4 + assert 0.2 < float((out.categorical_codes[:, 0] == 9).float().mean()) < 0.4 + assert torch.equal(data.x, torch.ones(200, 3)) # input untouched + assert build_finetune_augmentation({"drop_edge_p": 0.0, "drop_feat_p": 0.0}) is None + aug = build_finetune_augmentation({"drop_edge_p": 0.1, "drop_feat_p": 0.1}, unassigned_index=9) + assert aug(data).x.shape == data.x.shape + + +@pytest.mark.parametrize( + ("regime", "extra"), + [ + ("mil", []), + ("mil_frozen", []), + ("linear_probehead", ["task.head_init_path=/tmp/none.pt"]), + ("linear_ftall", ["task.head_init_path=/tmp/none.pt"]), + ("linear_ftall_reg", ["task.head_init_path=/tmp/none.pt"]), + ], +) +def test_regime_presets_compose(regime: str, extra: list[str]): + cfg = _compose([f"regime={regime}", *extra]) + assert cfg.regime_name == regime + assert cfg.model.encoder.conv_type == "gine" and int(cfg.model.encoder.num_layers) == 4 + assert cfg.data.sampler.runtime.unit_as_bag is True + assert int(cfg.data.val_sampler.runtime.subsample_count) == 1024 + assert str(cfg.scheduler.T_max) == "auto" and cfg.task.scheduler_interval == "step" + if regime.startswith("linear"): + assert int(cfg.model.graph_head.num_layers) == 1 + else: + assert int(cfg.model.graph_head.num_layers) == 2 + if regime == "linear_ftall_reg": + assert float(cfg.task.optimization.encoder_lr_scale) == pytest.approx(0.3) + assert float(cfg.task.finetune_augmentation.drop_edge_p) == pytest.approx(0.05) + assert float(cfg.model.encoder.dropout) == pytest.approx(0.2) + if regime == "linear_probehead": + assert ( + cfg.task.freeze_head is True and float(cfg.task.optimization.encoder_lr_scale) == 0.0 + ) + + +def test_regime_defaults_can_be_overridden(): + cfg = _compose( + [ + "regime=mil", + "model.encoder.num_layers=3", + "data.sampler.runtime.depth=3", + "model.graph_head.num_layers=1", + "data.sampler.runtime.subsample_count=64", + ] + ) + assert int(cfg.model.encoder.num_layers) == 3 and int(cfg.data.sampler.runtime.depth) == 3 + assert ( + int(cfg.model.graph_head.num_layers) == 1 + and int(cfg.data.sampler.runtime.subsample_count) == 64 + ) + + +def test_region_level_metrics_binary_and_categorical(): + from grass_mil.models.supervised_module import SupervisedModule + + logits = torch.tensor([[2.0, -1.0], [-1.0, 2.0], [1.5, 0.0], [-2.0, 1.0]]) + targets = torch.tensor([0, 1, 0, 1]) + m = SupervisedModule._region_level_metrics("categorical", logits, targets) + assert m["auroc"] == pytest.approx(1.0) and m["bal_acc"] == pytest.approx(1.0) + assert m["loss_region"] > 0 + mb = SupervisedModule._region_level_metrics( + "binary", torch.tensor([[2.0], [-2.0]]), torch.tensor([1.0, 0.0]) + ) + assert mb["auroc"] == pytest.approx(1.0) + + +def test_mil_training_with_scaled_groups_and_auto_weights(cfg_train, tmp_path): + """End-to-end: region accumulation, scaled parameter groups, auto class weights, + per-step cosine with T_max auto, region-level val metrics and test predictions.""" + from grass_mil.train import train + + cfg = _compose(["task=finetune_mil", "model=supervised_module", "scheduler=cosine_mil_step"]) + with open_dict(cfg): + cfg.data = OmegaConf.create( + { + "_target_": "tests.helpers.synthetic_datamodule.SyntheticBagDataModule", + "batch_size": 4, + "num_workers": 0, + "pin_memory": False, + "input_dim": 8, + } + ) + cfg.model.encoder.input_dim = 8 + cfg.task.optimization.encoder_lr_scale = 0.3 + cfg.task.optimization.head_lr_scale = 0.01 + cfg.task.optimization.head_weight_decay = 1.0e-3 + cfg.task.region_accumulation.hyperbatch_size = 2 + cfg.task.grad_clip_norm = 1.0 + cfg.callbacks = None + cfg.logger = None + cfg.paths.output_dir = str(tmp_path) + cfg.paths.root_dir = str(tmp_path) + cfg.trainer.max_epochs = 1 + cfg.trainer.min_epochs = 1 + cfg.trainer.accelerator = "cpu" + cfg.trainer.devices = 1 + cfg.trainer.default_root_dir = str(tmp_path) + cfg.extras.print_config = False + cfg.extras.enforce_tags = False + cfg.train = True + cfg.test = True + metrics, objects = train(cfg) + module = objects["model"] + assert module.loss_fn.class_weight is not None + assert module.loss_fn.class_weight.mean().item() == pytest.approx(1.0) + opt = objects["trainer"].optimizers[0] + group_lrs = sorted(g["initial_lr"] if "initial_lr" in g else g["lr"] for g in opt.param_groups) + assert len(opt.param_groups) == 3 + assert "val/auroc" in metrics and "test/auroc" in metrics + assert (Path(tmp_path) / "metrics_per_epoch.jsonl").exists() + assert (Path(tmp_path) / "test_predictions.csv").exists() + assert group_lrs[0] < group_lrs[-1] diff --git a/tests/models/test_fidelity_phase3.py b/tests/models/test_fidelity_phase3.py new file mode 100644 index 0000000..0f9b543 --- /dev/null +++ b/tests/models/test_fidelity_phase3.py @@ -0,0 +1,295 @@ +"""Phase 3: explicit splits, probe pre-fit, nested cross-validation.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest +import torch + +from tests.helpers.synthetic_cohort import cohort_overrides, compose_cfg, make_synthetic_cohort + +pytest.importorskip("torch_geometric") +pytest.importorskip("sklearn") + + +# ---------------------------------------------------------------------------- +# splits +# ---------------------------------------------------------------------------- + + +def test_explicit_split_from_lists_and_files(tmp_path): + from grass_mil.data.components.splits import ExplicitSplit + + (tmp_path / "tr.json").write_text(json.dumps(["a", "b"])) + (tmp_path / "va.txt").write_text("c\n\nd\n") + split = ExplicitSplit.from_dict( + {"train": str(tmp_path / "tr.json"), "val": str(tmp_path / "va.txt"), "test": ["e"]} + ) + assert split.train == ["a", "b"] and split.val == ["c", "d"] and split.test == ["e"] + assert split.lookup() == {"a": "train", "b": "train", "c": "val", "d": "val", "e": "test"} + assert ExplicitSplit.from_dict({"train": None, "val": None, "test": None}) is None + with pytest.raises(ValueError): + ExplicitSplit.from_dict({"train": ["a"], "val": ["a"], "test": ["b"]}) + # val == test is the refit convention and is tolerated + ExplicitSplit.from_dict({"train": ["a"], "val": ["b"], "test": ["b"]}) + + +def test_fold_json_roundtrip_and_duplicates(tmp_path): + from grass_mil.data.components.splits import load_fold_json, write_fold_json + + write_fold_json([["r1", "r2"], ["r3"]], tmp_path / "f.json") + assert load_fold_json(tmp_path / "f.json") == [["r1", "r2"], ["r3"]] + (tmp_path / "bad.json").write_text(json.dumps([["r1"], ["r1"]])) + with pytest.raises(ValueError): + load_fold_json(tmp_path / "bad.json") + + +def test_inner_folds_are_patient_safe_and_deterministic(tmp_path): + from grass_mil.data.components.splits import load_labels, make_inner_folds + + rows = [] + for p in range(9): + for r in range(2): + rows.append({"REGION_ID": f"P{p}_{r}", "STAS": p % 2, "PATIENT": f"P{p}"}) + pd.DataFrame(rows).to_csv(tmp_path / "labels.csv", index=False) + table = load_labels( + tmp_path / "labels.csv", + id_column="REGION_ID", + label_column="STAS", + patient_column="PATIENT", + ) + ids = [r["REGION_ID"] for r in rows] + folds_a = make_inner_folds(ids, table, k=3, seed=0) + folds_b = make_inner_folds(list(reversed(ids)), table, k=3, seed=0) + assert folds_a == folds_b # order of the id set does not matter + for tr, va in folds_a: + assert not (set(tr) & set(va)) + assert not ({table.group_of(r) for r in tr} & {table.group_of(r) for r in va}) + assert len({table.label_of(r) for r in tr}) == 2 + + +# ---------------------------------------------------------------------------- +# probe folding +# ---------------------------------------------------------------------------- + + +def test_probe_folding_reproduces_decision_function(): + from sklearn.linear_model import LogisticRegression + from sklearn.preprocessing import StandardScaler + + from grass_mil.models.training.probe import fold_probe_into_linear_head + + rng = np.random.default_rng(0) + X = rng.normal(size=(200, 12)) * rng.uniform(0.5, 3.0, size=12) + rng.normal(size=12) + y = (X[:, 0] - X[:, 3] > 0).astype(int) + sc = StandardScaler().fit(X) + clf = LogisticRegression(C=1.0, class_weight="balanced", max_iter=3000).fit(sc.transform(X), y) + W, B = fold_probe_into_linear_head( + clf.coef_, clf.intercept_, sc.mean_, sc.scale_, num_classes=2 + ) + assert torch.all(W[0] == 0) and B[0] == 0 + logits = torch.from_numpy(X).float() @ W.T + B + ref = clf.decision_function(sc.transform(X)) + assert np.allclose((logits[:, 1] - logits[:, 0]).numpy(), ref, atol=1e-4) + + +# ---------------------------------------------------------------------------- +# cohort fixture: precompute once per module +# ---------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def cohort(tmp_path_factory): + root = tmp_path_factory.mktemp("cohort") + c = make_synthetic_cohort(root, n_patients=8, regions_per_patient=2, n_cells=120) + cfg = compose_cfg("train", cohort_overrides(c, root / "out_precompute")) + import hydra + + dm = hydra.utils.instantiate(cfg.data) + dm.prepare_data() + return c + + +def test_precompute_records_patient_and_label(cohort): + index = json.loads((cohort.processed_dir / "processed_index.json").read_text()) + entries = index["entries"] + assert len(entries) == len(cohort.region_ids) + for e in entries: + assert e["patient_id"] == cohort.patient_of[e["region_id"]] + assert int(round(e["graph_y"][0])) == cohort.label_of[e["region_id"]] + data = torch.load(entries[0]["path"], weights_only=False) + assert data.patient_id == cohort.patient_of[entries[0]["region_id"]] + assert list(data.graph_label_names) == ["STAS"] + + +def test_explicit_split_repartitions_without_reprocessing(cohort, tmp_path): + import hydra + + ids = cohort.region_ids + cfg = compose_cfg( + "train", + cohort_overrides(cohort, tmp_path) + + [ + f"data.split.explicit.train=[{','.join(ids[:10])}]", + f"data.split.explicit.val=[{','.join(ids[10:13])}]", + f"data.split.explicit.test=[{','.join(ids[13:15])}]", + ], + ) + dm = hydra.utils.instantiate(cfg.data) + dm.setup("fit") + assert sorted(e.region_id for e in dm.dataset_train.entries) == sorted(ids[:10]) + assert sorted(e.region_id for e in dm.dataset_val.entries) == sorted(ids[10:13]) + assert sorted(e.region_id for e in dm.dataset_test.entries) == sorted(ids[13:15]) + cfg2 = compose_cfg( + "train", + cohort_overrides(cohort, tmp_path) + ["data.split.explicit.train=[nope]"], + ) + dm2 = hydra.utils.instantiate(cfg2.data) + with pytest.raises(ValueError, match="no processed graph"): + dm2.setup("fit") + + +def test_probe_cli_writes_folded_head(cohort, tmp_path): + from grass_mil.probe import run_probe + + ids = cohort.region_ids + assert len(ids) == 16 + cfg = compose_cfg( + "probe", + cohort_overrides(cohort, tmp_path) + + [ + "regime=linear_probehead", + "task.head_init_path=null", + f"data.split.explicit.train=[{','.join(ids[:10])}]", + f"data.split.explicit.val=[{','.join(ids[10:])}]", + f"data.split.explicit.test=[{','.join(ids[10:])}]", + "probe.roots_per_unit=16", + f"probe.output_path={tmp_path}/probe_head.pt", + ], + ) + summary = run_probe(cfg) + head = torch.load(tmp_path / "probe_head.pt", weights_only=False) + W = head["graph_head.net.0.weight"] + assert W.shape == (2, 16) and torch.all(W[0] == 0) + assert "val_region_auroc" in summary["metrics"] + assert summary["settings"]["roots_per_unit"] == 16 + assert summary["settings"]["n_train_regions"] == 10 + assert (tmp_path / "probe_head.json").exists() + + +# ---------------------------------------------------------------------------- +# nested CV +# ---------------------------------------------------------------------------- + + +def test_best_epoch_from_jsonl(tmp_path): + from grass_mil.nested_cv import best_epoch_from_jsonl + + p = tmp_path / "m.jsonl" + p.write_text( + "\n".join( + json.dumps(r) + for r in [ + {"epoch": 0, "val/auroc": 0.5}, + {"epoch": 1, "val/auroc": 0.7}, + {"epoch": 2, "val/auroc": 0.7}, + {"epoch": 3, "val/auroc": 0.6}, + ] + ) + ) + assert best_epoch_from_jsonl(p, metric="val/auroc") == ( + 2, + "val/auroc", + ) # earliest tie, 1-based + assert best_epoch_from_jsonl(p, metric="val/loss", mode="min") == (4, "last") + assert best_epoch_from_jsonl(p, metric="val/x", fallback_metric="val/auroc") == ( + 1, + "val/auroc", + ) + + +@pytest.mark.parametrize("regime", ["mil", "linear_probehead"]) +def test_nested_cv_smoke(cohort, tmp_path, regime): + from grass_mil.nested_cv import run_nested_cv + + out = tmp_path / regime + cfg = compose_cfg( + "nested_cv", + cohort_overrides(cohort, out) + + [ + f"regime={regime}", + f"nested_cv.folds_file={cohort.folds_path}", + "nested_cv.inner_k=2", + "nested_cv.outer_folds=[0,1]", + "nested_cv.probe.roots_per_unit=16", + "trainer.max_epochs=2", + ], + ) + summary = run_nested_cv(cfg) + assert summary["n_outer"] == 2 and not summary["complete"] + assert len(summary["auc_folds"]) == 2 + assert all(np.isfinite(a) for a in summary["auc_folds"]) + assert summary["probe"] == (regime == "linear_probehead") + oof = pd.read_csv(out / "oof_predictions.csv") + expected = sorted(cohort.folds[0] + cohort.folds[1]) + assert sorted(oof["region_id"]) == expected # one OOF prediction per held-out region + assert set(oof["patient_id"]) == {cohort.patient_of[r] for r in expected} + pat = pd.read_csv(out / "oof_patient_predictions.csv") + assert len(pat) == len({cohort.patient_of[r] for r in expected}) + for i in (0, 1): + res = json.loads((out / f"outer_{i}" / "result.json").read_text()) + assert 1 <= res["E"] <= 2 and len(res["inner_epochs"]) == 2 + assert (out / f"outer_{i}" / "refit" / "test_predictions.csv").exists() + if regime == "linear_probehead": + assert res["head_init_path"] and Path(res["head_init_path"]).exists() + # resume: a second call reuses every finished run + summary2 = run_nested_cv(cfg) + assert summary2["auc_folds"] == summary["auc_folds"] + + +def test_explicit_split_patch_id_kind_and_unit_key(): + from grass_mil.data.components.datasets import ProcessedIndexEntry + from grass_mil.data.components.splits import ExplicitSplit + from grass_mil.data.spatial_omics_datamodule import unit_key + + e = ProcessedIndexEntry(path="x", sample_id="s1", region_id="r1", patch_id="s1_t3") + assert ( + unit_key(e, "region") == "r1" + and unit_key(e, "patch") == "s1_t3" + and unit_key(e, "sample") == "s1" + ) + split = ExplicitSplit.from_dict( + {"train": ["s1_t3"], "val": None, "test": None, "id_kind": "patch"} + ) + assert split.id_kind == "patch" + with pytest.raises(ValueError): + ExplicitSplit.from_dict({"train": ["a"], "id_kind": "tile"}) + + +def test_patch_bags_log_region_level_metrics(cohort, tmp_path): + """Tiles as bags (task.bag_key=patch_id): validation AUROC is computed per tile.""" + from grass_mil.train import train + + cfg = compose_cfg( + "train", + cohort_overrides(cohort, tmp_path) + + [ + f"data.processed_dir={tmp_path / 'processed_tiles'}", + "data.sample_unit=tile", + "data.tiling.tile_size_um=300", + "data.tiling.stride_um=300", + "data.tiling.min_cells=5", + "data.split.split_by=region", + "task.bag_key=patch_id", + "trainer.max_epochs=1", + ], + ) + metrics, objects = train(cfg) + assert "val/auroc" in metrics and "test/auroc" in metrics + preds = pd.read_csv(tmp_path / "test_predictions.csv") + assert preds["bag_id"].str.contains("patch").all() + assert preds["bag_id"].nunique() == len(preds) > len(set(preds["region_id"])) diff --git a/tests/models/test_fidelity_phase4.py b/tests/models/test_fidelity_phase4.py new file mode 100644 index 0000000..3bfc6b2 --- /dev/null +++ b/tests/models/test_fidelity_phase4.py @@ -0,0 +1,236 @@ +"""Phase 4: BGRL "A3" pretraining fidelity (merged-unit sampling, momentum schedule, +legacy checkpoint import, preset end to end).""" + +from __future__ import annotations + + +import numpy as np +import pytest +import torch + +from tests.helpers.synthetic_cohort import cohort_overrides, compose_cfg, make_synthetic_cohort +from tests.models.test_fidelity_phase2 import _runtime, _units + +pytest.importorskip("torch_geometric") +pytest.importorskip("torch_sparse") + + +def test_merge_graph_units_offsets_edges_and_tracks_units(): + from grass_mil.data.components.merge import merge_graph_units + + units = _units(3, 40) + merged = merge_graph_units(units) + assert merged.num_nodes == 120 + assert merged.x.shape == (120, 4) + assert merged.edge_index.shape[1] == 3 * 160 + assert merged.edge_index[:, 160:320].min() >= 40 and merged.edge_index[:, 160:320].max() < 80 + assert merged.unit_index.tolist() == [0] * 40 + [1] * 40 + [2] * 40 + assert merged.unit_region_ids == ["region_0", "region_1", "region_2"] + assert merged.unit_graph_y.shape == (3, 1) + + +def test_merge_units_sampler_mixes_regions_and_covers_all_roots(): + from grass_mil.data.components.samplers import get_sampler_strategy + + units = _units(3, 40) + strategy = get_sampler_strategy( + { + "name": "shadow_custom", + "kwargs": {}, + "runtime": _runtime( + merge_units=True, subgraph_batch_size=32, depth=1, num_neighbors=-1 + ), + } + ) + loader = strategy.build_dataset_loader(dataset=units, batch_size=1, shuffle=True) + assert len(loader) == int(np.ceil(120 / 32)) + batches = list(loader) + assert len(batches) == len(loader) + n_graphs = sum(int(b.ptr.numel() - 1) for b in batches) + assert n_graphs == 120 # every root of every unit exactly once + # A full-size batch spans more than one unit: units are pooled, not iterated. + first = batches[0] + unit_of_graph = first.unit_index[first.ptr[:-1]] + assert len(set(unit_of_graph.tolist())) > 1 + + +def test_momentum_schedule_cosine_to_final_without_warmup(): + from grass_mil.models.training.ssl_runtime import CosineWarmup + + sched = CosineWarmup(base_value=0.99, warmup_steps=0, total_steps=1000, min_value=1.0) + assert sched.value(0) == pytest.approx(0.99) + assert sched.value(500) == pytest.approx(0.995) + assert sched.value(1000) == pytest.approx(1.0) + assert all(sched.value(s) <= sched.value(s + 1) + 1e-12 for s in range(999)) + + +def test_bgrl_module_reads_momentum_final_and_zero_momentum_warmup(): + from grass_mil.models.bgrl_module import BGRLModule + + module = BGRLModule( + encoder={"input_dim": 4, "hidden_dim": 8, "out_dim": 8, "num_layers": 1}, + ssl={"method": "bgrl", "predictor": {"hidden_size": 8}}, + task={ + "warmup_steps": 100, + "total_steps": 1000, + "momentum": 0.99, + "momentum_final": 1.0, + "momentum_warmup_steps": 0, + }, + ) + assert module._momentum_min == 1.0 + assert module._momentum_warmup_steps == 0 + legacy = BGRLModule( + encoder={"input_dim": 4, "hidden_dim": 8, "out_dim": 8, "num_layers": 1}, + ssl={"method": "bgrl", "predictor": {"hidden_size": 8}}, + task={"warmup_steps": 100, "total_steps": 1000, "momentum": 0.99, "momentum_min": 0.99}, + ) + assert legacy._momentum_min == 0.99 and legacy._momentum_warmup_steps == 100 + + +def test_spacegm_bgrl_remap_loads_into_public_encoder(): + from grass_mil.models.components.backbones import EncoderConfig + from grass_mil.models.components.factory import build_encoder + from grass_mil.models.training.checkpoint_init import remap_encoder_keys + + cfg = EncoderConfig( + input_dim=1, + hidden_dim=16, + out_dim=16, + num_layers=2, + conv_type="gine", + norm="layernorm", + pooling="max", + use_edge_attr=True, + edge_attr_dim=1, + edge_feature_index=0, + train_eps=True, + categorical_embedding={ + "label": "cell_type", + "num_embeddings": 5, + "column_index": 0, + "reserve_unassigned": True, + }, + ) + enc = build_encoder(cfg) + ref = {k: v.clone() for k, v in enc.state_dict().items()} + legacy = {} + for k, v in ref.items(): + if k == "input_proj.embedding.weight": + legacy["online_encoder.x_embedding.weight"] = v + 1 + elif k.startswith("input_proj.linear."): + legacy[k.replace("input_proj.linear.", "online_encoder.feat_embedding.")] = v + 1 + elif k.startswith("layers."): + i, rest = k.split(".", 2)[1], k.split(".", 2)[2] + legacy[f"online_encoder.gnns.{i}.gine.{rest}"] = v + 1 + legacy[f"online_encoder.gnns.{i}.mlp.0.weight"] = torch.zeros(1) + legacy[f"online_encoder.gnns.{i}.edge_embedding.weight"] = torch.zeros(4, 16) + elif k.startswith("norms."): + legacy[k.replace("norms.", "online_encoder.batch_norms.")] = v + 1 + legacy["target_encoder.x_embedding.weight"] = torch.zeros(1) + legacy["predictor.net.0.weight"] = torch.zeros(1) + remapped = remap_encoder_keys(legacy, encoder_init_map="spacegm_bgrl") + assert set(remapped) == {f"encoder.{k}" for k in ref} + stripped = {k[len("encoder.") :]: v for k, v in remapped.items()} + enc.load_state_dict(stripped, strict=True) + assert torch.allclose(enc.state_dict()["layers.0.eps"], ref["layers.0.eps"] + 1) + with pytest.raises(ValueError): + remap_encoder_keys({"foo": torch.zeros(1)}, encoder_init_map="spacegm_bgrl") + + +@pytest.fixture(scope="module") +def cohort(tmp_path_factory): + root = tmp_path_factory.mktemp("cohort4") + c = make_synthetic_cohort(root, n_patients=4, regions_per_patient=2, n_cells=100) + import hydra + + cfg = compose_cfg("train", cohort_overrides(c, root / "out_precompute")) + dm = hydra.utils.instantiate(cfg.data) + dm.prepare_data() + return c + + +def test_pretrain_a3_preset_then_finetune_from_checkpoint(cohort, tmp_path): + from grass_mil.train import train + + pre_out = tmp_path / "pretrain" + base = [ + o + for o in cohort_overrides(cohort, pre_out) + if not o.startswith(("regime=", "data.val_sampler", "model.attention", "task.")) + ] + cfg = compose_cfg( + "train", + base + + [ + "experiment=grass_mil_pretrain_a3", + "task.total_steps=4", + "task.warmup_steps=1", + "task.lr_warmup.warmup_steps=1", + "task.augmentation.feature_noise_columns=[area]", + "callbacks.model_checkpoint.every_n_train_steps=2", + "trainer.precision=32", + "data.sampler.runtime.depth=2", + "data.sampler.runtime.subgraph_batch_size=16", + ], + ) + assert cfg.task.momentum_final == 1.0 and cfg.data.sampler.runtime.merge_units is True + assert cfg.trainer.max_steps == 4 and cfg.scheduler.T_max == "auto" + metrics, objects = train(cfg) + module = objects["model"] + assert module.encoder.dropout.p == pytest.approx(0.1) + assert "train/loss" in metrics + ckpts = sorted((pre_out / "checkpoints").glob("*.ckpt")) + names = {p.name for p in ckpts} + assert "last.ckpt" in names and any(n.startswith("step_") for n in names) + # the scheduler covered total_steps - warmup and momentum reached its final value + # last update used the momentum of zero-based step 3 of 4 (cosine 0.99 -> 1.0) + import math + + expected = 1.0 + (0.99 - 1.0) * 0.5 * (1.0 + math.cos(math.pi * 3 / 4)) + assert float(metrics["train/momentum"]) == pytest.approx(expected, abs=1e-5) + pre_state = torch.load(pre_out / "checkpoints" / "last.ckpt", weights_only=False)["state_dict"] + enc_keys = [ + k for k in pre_state if k.startswith("ssl_model.online_encoder.layers.0.nn.0.weight") + ] + assert enc_keys + + ft_out = tmp_path / "finetune" + ft_cfg = compose_cfg( + "train", + cohort_overrides(cohort, ft_out) + + [ + f"model.init_from_ckpt={pre_out / 'checkpoints' / 'last.ckpt'}", + "trainer.max_epochs=1", + ], + ) + _, ft_objects = train(ft_cfg) + ft_module = ft_objects["model"] + # the fine-tuned model started from the pretrained encoder weights + loaded = ft_module.encoder.state_dict()["norms.0.weight"].detach().cpu() + pretrained = pre_state["ssl_model.online_encoder.norms.0.weight"].detach().cpu() + assert torch.allclose(loaded, pretrained, atol=5e-3) + assert ft_module.encoder.dropout.p == 0.0 + + +def test_zero_validation_callback_filter_keeps_interpolations_resolvable(): + from omegaconf import OmegaConf + + from grass_mil.train import _filter_val_monitor_callbacks + + cfg = OmegaConf.create( + { + "paths": {"output_dir": "/tmp/run"}, + "callbacks": { + "model_checkpoint": { + "_target_": "x", + "dirpath": "${paths.output_dir}/checkpoints", + "monitor": None, + }, + "early_stopping": {"_target_": "y", "monitor": "val/loss"}, + }, + } + ) + filtered, removed = _filter_val_monitor_callbacks(cfg.callbacks) + assert removed == ["early_stopping"] + assert filtered.model_checkpoint.dirpath == "/tmp/run/checkpoints" diff --git a/tests/models/test_fidelity_phase5.py b/tests/models/test_fidelity_phase5.py new file mode 100644 index 0000000..be19dd3 --- /dev/null +++ b/tests/models/test_fidelity_phase5.py @@ -0,0 +1,273 @@ +"""Phase 5: cell cache extraction, patient aggregation, evaluation statistics.""" + +from __future__ import annotations + +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest +import torch + +from tests.helpers.synthetic_cohort import cohort_overrides, compose_cfg, make_synthetic_cohort + +pytest.importorskip("torch_geometric") +pytest.importorskip("sklearn") + + +# ---------------------------------------------------------------------------- +# statistics +# ---------------------------------------------------------------------------- + + +def test_auroc_matches_sklearn_and_delong_identity_and_symmetry(): + from sklearn.metrics import roc_auc_score + + from grass_mil.evaluation import auroc, delong_test + + rng = np.random.default_rng(0) + y = rng.integers(0, 2, 80) + s1 = y + rng.normal(0, 0.5, 80) + s2 = rng.normal(size=80) + assert auroc(y, s1) == pytest.approx(roc_auc_score(y, s1)) + same = delong_test(y, s1, s1) + assert same.delta == 0.0 and same.p_value == pytest.approx(1.0) + d12 = delong_test(y, s1, s2) + d21 = delong_test(y, s2, s1) + assert d12.delta == pytest.approx(-d21.delta) and d12.p_value == pytest.approx(d21.p_value) + assert d12.auroc_1 == pytest.approx(roc_auc_score(y, s1)) + assert d12.p_value < 0.05 # informative vs random score on 80 units + + +def test_bootstrap_ci_brackets_point_and_group_bootstrap_runs(): + from grass_mil.evaluation import bootstrap_auroc_ci + + rng = np.random.default_rng(1) + y = rng.integers(0, 2, 60) + s = y + rng.normal(0, 1, 60) + ci = bootstrap_auroc_ci(y, s, n_boot=300, seed=0) + assert ci.lower <= ci.auroc <= ci.upper and ci.n_valid > 0 + groups = np.repeat(np.arange(30), 2) + ci_g = bootstrap_auroc_ci(y, s, n_boot=300, seed=0, groups=groups) + assert ci_g.lower <= ci_g.auroc <= ci_g.upper + + +def test_within_fold_standardisation_and_per_fold_auroc(): + from grass_mil.evaluation import per_fold_auroc, rank_within_fold, z_within_fold + + folds = np.array([0] * 5 + [1] * 5) + s = np.array([1, 2, 3, 4, 5, 10, 20, 30, 40, 50], dtype=float) + z = z_within_fold(s, folds) + assert z[:5].mean() == pytest.approx(0) and z[5:].std() == pytest.approx(1) + assert np.allclose(z[:5], z[5:]) # scale-free + r = rank_within_fold(s, folds) + assert np.allclose(r[:5], (np.arange(5) + 0.5) / 5) + y = np.array([0, 0, 1, 1, 1, 0, 1, 0, 1, 1]) + pf = per_fold_auroc(y, s, folds) + assert pf["n_folds"] == 2 and 0 <= pf["mean"] <= 1 and "fold_0" in pf + + +def test_operating_points_from_confusion_and_derived_quantities(): + from grass_mil.evaluation import ( + confusion_counts, + flag_top_within_fold, + likelihood_ratios, + metrics_from_confusion, + ppv_npv_at_prevalence, + threshold_points, + ) + + y = np.array([1, 1, 1, 1, 0, 0, 0, 0, 0, 0]) + pred = np.array([1, 1, 1, 0, 1, 1, 0, 0, 0, 0]) + c = confusion_counts(pred, y) + assert (c.tp, c.fp, c.fn, c.tn) == (3, 2, 1, 4) + m = metrics_from_confusion(c) + assert m["sensitivity"] == pytest.approx(0.75) and m["specificity"] == pytest.approx(4 / 6) + assert m["ppv"] == pytest.approx(0.6) and m["npv"] == pytest.approx(0.8) + lr = likelihood_ratios(0.75, 4 / 6) + assert lr["lr_plus"] == pytest.approx(0.75 / (1 - 4 / 6)) + r = ppv_npv_at_prevalence(0.75, 4 / 6, 0.4) + assert r["ppv"] == pytest.approx(0.75 * 0.4 / (0.75 * 0.4 + (1 / 3) * 0.6)) + folds = np.array([0] * 5 + [1] * 5) + score = np.arange(10, dtype=float) + flagged = flag_top_within_fold(score, folds, 0.4) + assert flagged.tolist() == [0, 0, 0, 1, 1, 0, 0, 0, 1, 1] # top 40 % of EACH fold + t = threshold_points(score, y) + assert set(t) == {"youden", "rule_out", "rule_in"} + + +# ---------------------------------------------------------------------------- +# patient aggregation +# ---------------------------------------------------------------------------- + + +def _payload(): + from grass_mil.inference.schemas import BatchPredictionPayload + + torch.manual_seed(0) + regions = ["P1_a", "P1_b", "P2_a"] + counts = [3, 2, 4] + inst_logits = torch.randn(sum(counts), 2) + inst_attn = torch.randn(sum(counts), 2) + inst_region = [r for r, n in zip(regions, counts) for _ in range(n)] + bag_logits = torch.stack( + [ + ( + inst_logits[[i for i, rr in enumerate(inst_region) if rr == r]] + * torch.softmax( + inst_attn[[i for i, rr in enumerate(inst_region) if rr == r]], dim=0 + ) + ).sum(0) + for r in regions + ] + ) + return ( + BatchPredictionPayload( + bag_ids=regions, + bag_logits=bag_logits, + bag_targets=torch.tensor([[1.0], [1.0], [0.0]]), + bag_attention=None, + row_region_ids=regions, + row_sample_ids=regions, + instance_logits=inst_logits, + instance_attention_logits=inst_attn, + instance_patch_ids=[f"p{i}" for i in range(sum(counts))], + instance_bag_ids=inst_region, + instance_region_ids=inst_region, + instance_sample_ids=inst_region, + ), + inst_logits, + inst_attn, + inst_region, + ) + + +def test_patient_mean_probability_and_merged_bag_aggregation(): + from grass_mil.inference.aggregation import aggregate_group_logits + + payload, inst_logits, inst_attn, inst_region = _payload() + gmap = {"P1_a": "P1", "P1_b": "P1", "P2_a": "P2"} + mean_p = aggregate_group_logits( + payload, mode="mean_probability", bag_scope="patient", group_map=gmap + ) + assert mean_p.bag_ids == ["P1", "P2"] + probs = torch.softmax(mean_p.bag_logits, dim=1) + expected = torch.softmax(payload.bag_logits[:2], dim=1).mean(0) + assert torch.allclose(probs[0], expected, atol=1e-6) + assert mean_p.bag_targets[0].item() == 1.0 and mean_p.bag_targets[1].item() == 0.0 + + merged = aggregate_group_logits( + payload, mode="attention_weighted", bag_scope="patient", group_map=gmap + ) + idx = [i for i, r in enumerate(inst_region) if gmap[r] == "P1"] + direct = (inst_logits[idx] * torch.softmax(inst_attn[idx], dim=0)).sum(0) + assert torch.allclose(merged.bag_logits[0], direct, atol=1e-6) + with pytest.raises(ValueError, match="patient"): + aggregate_group_logits(payload, mode="mean_probability", bag_scope="patient") + + +# ---------------------------------------------------------------------------- +# extraction + scores on the synthetic cohort +# ---------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def cohort(tmp_path_factory): + root = tmp_path_factory.mktemp("cohort5") + c = make_synthetic_cohort(root, n_patients=4, regions_per_patient=2, n_cells=80) + import hydra + + cfg = compose_cfg("train", cohort_overrides(c, root / "out_precompute")) + dm = hydra.utils.instantiate(cfg.data) + dm.prepare_data() + return c + + +def test_extract_cell_cache_covers_every_cell(cohort, tmp_path): + from grass_mil.extract import run_extract + from grass_mil.inference.cell_cache import CellCache + from grass_mil.train import train + + train_out = tmp_path / "train" + cfg = compose_cfg("train", cohort_overrides(cohort, train_out) + ["trainer.max_epochs=1"]) + train(cfg) + ckpt = train_out / "checkpoints" / "last.ckpt" + assert ckpt.exists() + + ext_out = tmp_path / "extract" + ecfg = compose_cfg( + "extract", + cohort_overrides(cohort, ext_out) + + [f"+extract.models.final={ckpt}", "extract.batch_size=32", "extract.device=cpu"], + ) + result = run_extract(ecfg) + cache = CellCache.open(Path(result["models"][0])) + assert sorted(cache.regions) == sorted(cohort.region_ids) + assert len(cache.bias) == 2 and set(cache.cell_type_names) == {"A", "B", "C", "D"} + rid = cache.regions[0] + arrays = cache.load(rid) + n = cache.n_cells(rid) + assert arrays["E"].shape == (n, 16) and arrays["E"].dtype == np.float16 + assert arrays["LG"].shape == (n, 2) and arrays["AR"].shape == (n, 2) + assert arrays["ct"].shape == (n,) and arrays["pos"].shape == (n, 2) + assert arrays["edge_index"].shape[0] == 2 and arrays["edge_index"].max() < n + assert np.isfinite(arrays["E"].astype(np.float32)).all() + assert cache.label_of(rid) == float(cohort.label_of[rid]) + # row order = cell order of the region table (first cell's type matches the CSV) + csv = pd.read_csv(cohort.manifest_path).set_index("region_id").loc[rid, "input_path"] + types = pd.read_csv(csv)["cell_type"].tolist() + assert [cache.cell_type_names[c] for c in arrays["ct"]] == types + + +def test_scores_cli_on_synthetic_oof_table(tmp_path): + from grass_mil.evaluation.cli import run_scores + + rng = np.random.default_rng(0) + rows = [] + for outer in range(3): + for p in range(20): + pid = f"o{outer}_P{p}" + label = int(p % 2 == 0) + for core in range(2): + rows.append( + { + "outer": outer, + "region_id": f"{pid}_c{core}", + "patient_id": pid, + "label": label, + "prob_class_1": 0.3 + 0.3 * label + rng.normal(0, 0.15) + 0.1 * outer, + "other": rng.uniform(), + "subtype": "A" if p < 10 else "B", + } + ) + table = tmp_path / "oof.csv" + pd.DataFrame(rows).to_csv(table, index=False) + cfg = compose_cfg( + "scores", + [ + f"input={table}", + f"output_dir={tmp_path / 'scores'}", + "scores=[prob_class_1,other]", + "strata=[subtype]", + "n_boot=200", + ], + ) + summary = run_scores(cfg) + assert summary["n"] == 60 # patient level + first = summary["scores"][0] + assert first["auroc"] > 0.8 and first["lower"] <= first["auroc"] <= first["upper"] + assert first["perfold_n_folds"] == 3 + assert summary["delong"][0]["p_value"] < 0.05 + out = tmp_path / "scores" + for name in ( + "scores_summary.csv", + "delong.csv", + "operating_points.csv", + "strata.csv", + "ppv_npv_by_prevalence.csv", + "report_scores.md", + ): + assert (out / name).exists(), name + ops = pd.read_csv(out / "operating_points.csv") + top30 = ops[(ops.score == "prob_class_1") & (ops.rule.str.startswith("top 30"))].iloc[0] + assert abs(top30["flagged"] - 0.3) < 0.06 diff --git a/tests/spatial_stats/__init__.py b/tests/spatial_stats/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/spatial_stats/test_fidelity_phase8.py b/tests/spatial_stats/test_fidelity_phase8.py new file mode 100644 index 0000000..27513eb --- /dev/null +++ b/tests/spatial_stats/test_fidelity_phase8.py @@ -0,0 +1,171 @@ +"""Phase 8: classical spatial statistics.""" + +from __future__ import annotations + +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +from tests.helpers.synthetic_cohort import cohort_overrides, compose_cfg, make_synthetic_cohort + +pytest.importorskip("scipy") + + +def _delaunay_edges(coords): + from scipy.spatial import Delaunay + + tri = Delaunay(coords) + e = set() + for s in tri.simplices: + for i in range(3): + a, b = int(s[i]), int(s[(i + 1) % 3]) + e.add((min(a, b), max(a, b))) + return np.array(sorted(e)).T + + +def test_nhood_zscores_random_vs_segregated(): + from grass_mil.spatial_stats import nhood_enrichment_zscores + + rng = np.random.default_rng(0) + coords = rng.uniform(0, 100, (400, 2)) + ei = _delaunay_edges(coords) + random_labels = rng.integers(0, 3, 400) + z = nhood_enrichment_zscores(random_labels, ei, n_groups=3, n_perms=100, seed=0) + assert z.shape == (3, 3) and np.allclose(z, z.T) and np.abs(z).max() < 3.5 + segregated = (coords[:, 0] > 50).astype(int) + z2 = nhood_enrichment_zscores(segregated, ei, n_groups=3, n_perms=100, seed=0) + assert z2[0, 0] > 3 and z2[1, 1] > 3 and z2[0, 1] < -3 + assert np.isnan(z2[2]).all() # absent group + + +def test_window_composition_and_cellular_neighbourhoods(): + from grass_mil.spatial_stats import cellular_neighbourhoods, window_composition + + rng = np.random.default_rng(1) + coords = rng.uniform(0, 100, (300, 2)) + labels = (coords[:, 0] > 50).astype(int) + W = window_composition(coords, labels, n_groups=2, k=10) + assert W.shape == (300, 2) and np.allclose(W.sum(1), 1.0) + assert W[labels == 0, 0].mean() > 0.8 + cn, centres = cellular_neighbourhoods([W, W[:50]], n_cn=2, sample_size=100, seed=0, n_init=2) + assert len(cn) == 2 and cn[0].shape == (300,) and centres.shape == (2, 2) + + +def test_crossk_csr_vs_clustered_and_contacts(): + from grass_mil.spatial_stats import contact_fractions, cross_k + + rng = np.random.default_rng(2) + coords = rng.uniform(0, 1000, (2000, 2)) + labels = rng.integers(0, 2, 2000) + k = cross_k( + coords, labels, group_names=["T", "X"], reference=["T"], partners=["X"], radii=[50, 100] + ) + assert abs(k["K_X_r50"] - 1.0) < 0.3 and abs(k["K_X_r100"] - 1.0) < 0.2 + # every X cell placed next to a T cell: strong attraction at short range + coords2 = coords.copy() + x_idx = np.where(labels == 1)[0] + t_idx = rng.choice(np.where(labels == 0)[0], x_idx.size) + coords2[x_idx] = coords[t_idx] + rng.normal(0, 3, (x_idx.size, 2)) + k2 = cross_k( + coords2, labels, group_names=["T", "X"], reference=["T"], partners=["X"], radii=[20] + ) + assert k2["K_X_r20"] > 1.5 + ei = _delaunay_edges(coords2) + c = contact_fractions( + labels, ei, group_names=["T", "X"], reference=["T"], partners=["X"], infiltrating=["X"] + ) + assert 0 <= c["contact_X"] <= 1 and 0 <= c["homotypic"] <= 1 and "infiltration" in c + + +def test_mann_whitney_table_and_bh(): + from grass_mil.spatial_stats import bh_adjust, mann_whitney_table, patient_mean + + rng = np.random.default_rng(3) + df = pd.DataFrame( + {"a": np.r_[rng.normal(1, 1, 20), rng.normal(0, 1, 20)], "b": rng.normal(size=40)}, + index=[f"r{i}" for i in range(40)], + ) + y = pd.Series(np.r_[np.ones(20), np.zeros(20)].astype(int), index=df.index) + T = mann_whitney_table(df, y) + assert list(T.columns) == [ + "metric", + "n_pos", + "n_neg", + "mean_pos", + "mean_neg", + "diff", + "p", + "q", + ] + assert T.set_index("metric").loc["a", "p"] < 0.05 + q = bh_adjust([0.01, 0.02, 0.5, np.nan]) + assert np.isnan(q[3]) and q[0] <= q[2] + pm = patient_mean(df, {f"r{i}": f"p{i // 2}" for i in range(40)}) + assert len(pm) == 20 + + +@pytest.fixture(scope="module") +def cache_dir(tmp_path_factory): + pytest.importorskip("torch_geometric") + import hydra + + from grass_mil.extract import run_extract + from grass_mil.train import train + + root = tmp_path_factory.mktemp("cohort8") + c = make_synthetic_cohort(root, n_patients=12, regions_per_patient=2, n_cells=80) + hydra.utils.instantiate( + compose_cfg("train", cohort_overrides(c, root / "pre")).data + ).prepare_data() + train_out = root / "train" + train(compose_cfg("train", cohort_overrides(c, train_out) + ["trainer.max_epochs=1"])) + ecfg = compose_cfg( + "extract", + cohort_overrides(c, root / "extract") + + [ + f"+extract.models.final={train_out / 'checkpoints' / 'last.ckpt'}", + "extract.device=cpu", + ], + ) + return c, Path(run_extract(ecfg)["models"][0]) + + +def test_spatial_stats_cli_finds_planted_niche(cache_dir, tmp_path): + from grass_mil.spatial_stats.cli import run + + c, model_dir = cache_dir + cfg = compose_cfg( + "spatial_stats", + [ + f"cells_dir={model_dir}", + f"output_dir={tmp_path / 'spatial'}", + f"labels.label_file={c.label_path}", + "labels.label_column=STAS", + "labels.patient_column=PATIENT", + "reference=[C]", + "partners=[D,A]", + "infiltrating=[D]", + "nhood.n_perms=50", + "cn.n_cn=3", + "cn.sample_size=500", + "min_per_group=3", + "crossk.radii=[30,60]", + ], + ) + run(cfg) + out = tmp_path / "spatial" + for name in ( + "nhood_tests.csv", + "cn_tests.csv", + "contact_tests.csv", + "crossk_tests.csv", + "report_spatial.md", + ): + assert (out / name).exists(), name + nh = pd.read_csv(out / "nhood_tests.csv").set_index("metric") + # the planted niche: D cells cluster together (and near C) only in positive regions + assert nh.loc["D|D", "diff"] > 0 and nh.loc["D|D", "p"] < 0.05 + ck = pd.read_csv(out / "crossk_tests.csv").set_index("metric") + assert ck.loc["K_D_r30", "mean_pos"] > ck.loc["K_D_r30", "mean_neg"] diff --git a/tests/test_e2e_synthetic.py b/tests/test_e2e_synthetic.py new file mode 100644 index 0000000..6216c1a --- /dev/null +++ b/tests/test_e2e_synthetic.py @@ -0,0 +1,227 @@ +"""End-to-end smoke test of the public analysis chain on the synthetic cohort. + +precompute -> BGRL A3 preset (a few steps) -> nested CV (regime=mil and the +probe regime) -> extract -> niches -> metaclusters -> spatial statistics -> +scores. Asserts that every artefact exists, AUROCs are finite, the attribution +identity holds and the planted niche (D cells packed around C cells in positive +regions) is the top enriched cluster of at least one fold model. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +from tests.helpers.synthetic_cohort import cohort_overrides, compose_cfg, make_synthetic_cohort + +pytest.importorskip("torch_geometric") +pytest.importorskip("torch_sparse") + + +@pytest.mark.slow +def test_end_to_end_synthetic_chain(tmp_path): + import hydra + + from grass_mil.evaluation.cli import run_scores + from grass_mil.extract import run_extract + from grass_mil.interpretability.metaclusters_cli import run_metaclusters + from grass_mil.interpretability.niches_cli import run_niches + from grass_mil.nested_cv import run_nested_cv + from grass_mil.spatial_stats.cli import run as run_spatial + from grass_mil.train import train + + root = tmp_path / "cohort" + c = make_synthetic_cohort( + root, n_patients=10, regions_per_patient=2, n_cells=120, n_outer_folds=2 + ) + hydra.utils.instantiate( + compose_cfg("train", cohort_overrides(c, root / "pre")).data + ).prepare_data() + + # 1. pretraining preset, a handful of steps + pre_out = root / "pretrain" + base = [ + o + for o in cohort_overrides(c, pre_out) + if not o.startswith(("regime=", "data.val_sampler", "model.attention", "task.")) + ] + train( + compose_cfg( + "train", + base + + [ + "experiment=grass_mil_pretrain_a3", + "task.total_steps=4", + "task.warmup_steps=1", + "task.lr_warmup.warmup_steps=1", + "task.augmentation.feature_noise_columns=[area]", + "callbacks.model_checkpoint.every_n_train_steps=2", + "trainer.precision=32", + "data.sampler.runtime.depth=2", + "data.sampler.runtime.subgraph_batch_size=16", + ], + ) + ) + bgrl = pre_out / "checkpoints" / "last.ckpt" + assert bgrl.exists() + + # 2. nested CV, default regime and the probe regime + summaries = {} + for regime in ("mil", "linear_probehead"): + out = root / f"nested_{regime}" + cfg = compose_cfg( + "nested_cv", + cohort_overrides(c, out) + + [ + f"regime={regime}", + f"model.init_from_ckpt={bgrl}", + f"nested_cv.folds_file={c.folds_path}", + "nested_cv.inner_k=2", + "nested_cv.probe.roots_per_unit=16", + "trainer.max_epochs=2", + ], + ) + summaries[regime] = run_nested_cv(cfg) + assert summaries[regime]["complete"] + assert all(np.isfinite(a) for a in summaries[regime]["auc_folds"]) + assert (out / "oof_predictions.csv").exists() and ( + out / "oof_patient_predictions.csv" + ).exists() + + # 3. scores on the OOF table + scores = run_scores( + compose_cfg( + "scores", + [ + f"input={root / 'nested_mil' / 'oof_predictions.csv'}", + f"output_dir={root / 'scores'}", + "n_boot=100", + ], + ) + ) + assert scores["n"] == 10 and np.isfinite(scores["scores"][0]["auroc"]) + + # 4. extract both fold refits + ext = run_extract( + compose_cfg( + "extract", + cohort_overrides(c, root / "extract") + + [ + f"extract.nested_cv_summary={root / 'nested_mil' / 'nested_cv_summary.json'}", + "extract.device=cpu", + ], + ) + ) + cells_dir = Path(ext["output_dir"]) + assert sorted(p.name for p in cells_dir.iterdir()) == ["fold0", "fold1"] + # a probe regime extracts from its refit checkpoints without a probe file + ext_lin = run_extract( + compose_cfg( + "extract", + cohort_overrides(c, root / "extract_lin") + + [ + "regime=linear_probehead", + f"extract.nested_cv_summary={root / 'nested_linear_probehead' / 'nested_cv_summary.json'}", + "extract.device=cpu", + ], + ) + ) + assert len(ext_lin["models"]) == 2 + + # 5. niches of one model (default backend with a small min_cluster_size) and a k-means override. + # The chain is a smoke test: a two-epoch model on 20 tiny regions is not expected to rank + # the planted niche first, so the checks here are structural; the planted niche is + # detected model-free by the spatial statistics in step 7. + for fold in ("fold0", "fold1"): + res = run_niches( + compose_cfg( + "niches", + [ + f"cells_dir={cells_dir / fold}", + f"output_dir={root / 'niches' / fold}", + "clustering.params.min_cluster_size=20", + "sample_size=null", + "n_boot=50", + "umap.enabled=false", + ], + ) + ) + assert res["n_clusters"] >= 1 + table = pd.read_csv(root / "niches" / fold / "clusters.csv") + cells = pd.read_csv(root / "niches" / fold / "cells.csv") + assert table["share"].sum() == pytest.approx(1.0) + assert ( + np.isfinite(table["relattn"]).all() + and (table["relattn_lo"] <= table["relattn_hi"]).all() + ) + assert set(cells["cluster"]) == set(table["cluster_id"]) + held_out = json.loads((cells_dir / fold / "index.json").read_text())["held_out_regions"] + assert set(cells["region_id"]) == set(held_out) + km = run_niches( + compose_cfg( + "niches", + [ + f"cells_dir={cells_dir / 'fold0'}", + f"output_dir={root / 'niches_km'}", + "clustering.method=kmeans", + "clustering.n_clusters=4", + "sample_size=null", + "n_boot=20", + "umap.enabled=false", + ], + ) + ) + assert km["n_clusters"] == 4 + + # 6. metaclusters, fixed threshold + meta = run_metaclusters( + compose_cfg( + "metaclusters", + [ + f"cells_dir={cells_dir}", + f"output_dir={root / 'meta'}", + f"labels.label_file={c.label_path}", + "labels.label_column=STAS", + "labels.patient_column=PATIENT", + "clustering.params.min_cluster_size=20", + "sample_size=null", + "n_boot=50", + "match.threshold=0.4", + "match.min_folds=2", + "transfer.min_cells=10", + "basis.k=4", + "basis.sample_size=2000", + "basis.n_init=2", + ], + ) + ) + assert ( + meta["match"]["n_metaclusters"] >= 1 + and (root / "meta" / "basis" / "basis_attribution.csv").exists() + ) + + # 7. spatial statistics + sp = run_spatial( + compose_cfg( + "spatial_stats", + [ + f"cells_dir={cells_dir / 'fold0'}", + f"output_dir={root / 'spatial'}", + f"labels.label_file={c.label_path}", + "labels.label_column=STAS", + "labels.patient_column=PATIENT", + "reference=[C]", + "partners=[D]", + "nhood.n_perms=30", + "cn.n_cn=3", + "cn.sample_size=500", + "min_per_group=3", + ], + ) + ) + nh = pd.read_csv(sp["nhood_tests"]).set_index("metric") + assert nh.loc["D|D", "diff"] > 0 diff --git a/tests/test_train_regimes.py b/tests/test_train_regimes.py index ae54855..ad66c5e 100644 --- a/tests/test_train_regimes.py +++ b/tests/test_train_regimes.py @@ -1,7 +1,6 @@ from pathlib import Path import pytest -import rootutils from hydra.core.hydra_config import HydraConfig from omegaconf import OmegaConf, open_dict from grass_mil.train import train @@ -9,7 +8,16 @@ def _load_model_cfg(name: str): - return OmegaConf.load(CONFIGS_DIR / "model" / f"{name}.yaml") + # Model presets carry a defaults list (model/encoder, model/attention groups), + # so they must be composed through Hydra rather than loaded as plain YAML. + from hydra import compose, initialize_config_module + from hydra.core.global_hydra import GlobalHydra + + GlobalHydra.instance().clear() + with initialize_config_module(version_base="1.3", config_module="grass_mil.configs"): + cfg = compose(config_name="train", overrides=[f"model={name}"]) + GlobalHydra.instance().clear() + return cfg.model def _load_task_cfg(name: str): @@ -122,97 +130,3 @@ def test_training_regimes_fast_dev_run( HydraConfig().set_config(cfg_train) metric_dict, _ = train(cfg_train) assert "train/loss" in metric_dict - - -@pytest.mark.skip( - reason=( - "Dummy real-data fixture can be too small to satisfy non-empty train/val split constraints." - ) -) -def test_runtime_shadow_path_with_real_datamodule(cfg_train): - root = Path(rootutils.find_root(indicator=".project-root")) - with open_dict(cfg_train): - cfg_train.data = OmegaConf.load(CONFIGS_DIR / "data" / "spatial_omics.yaml") - cfg_train.data.raw_manifest_path = str(root / "data" / "dummy" / "manifest.csv") - cfg_train.data.processed_dir = str(root / "data" / "dummy" / "processed") - cfg_train.data.num_workers = 0 - cfg_train.data.pin_memory = False - cfg_train.data.sampler.runtime.enabled = True - cfg_train.data.sampler.runtime.depth = 2 - cfg_train.data.sampler.runtime.num_neighbors = 8 - cfg_train.data.sampler.runtime.subgraph_batch_size = 8 - - cfg_train.task = _load_task_cfg("pretrain_bgrl") - cfg_train.model = _load_model_cfg("bgrl_module") - cfg_train.optim = OmegaConf.load(CONFIGS_DIR / "optim" / "adamw.yaml") - cfg_train.scheduler = OmegaConf.load(CONFIGS_DIR / "scheduler" / "cosine_step.yaml") - cfg_train.model.task = cfg_train.task - cfg_train.model.optim = cfg_train.optim - cfg_train.model.scheduler = cfg_train.scheduler - cfg_train.model.encoder.input_dim = 0 - cfg_train.model._recursive_ = False - - cfg_train.callbacks = OmegaConf.create({}) - cfg_train.logger = None - cfg_train.trainer.fast_dev_run = True - cfg_train.trainer.accelerator = "cpu" - cfg_train.trainer.devices = 1 - cfg_train.train = True - cfg_train.test = False - - HydraConfig().set_config(cfg_train) - metric_dict, _ = train(cfg_train) - assert "train/loss" in metric_dict - - -def test_survival_regime_fast_dev_run(cfg_train, tmp_path): - """The survival preset must train end-to-end and log a c-index.""" - with open_dict(cfg_train): - cfg_train.data = OmegaConf.create( - { - "_target_": "tests.helpers.synthetic_datamodule.SyntheticBagDataModule", - "batch_size": 4, - "num_workers": 0, - "pin_memory": False, - "input_dim": 8, - "survival": True, - } - ) - cfg_train.task = _load_task_cfg("finetune_survival") - cfg_train.model = _load_model_cfg("supervised_module") - overrides = _load_task_model_overrides("finetune_survival") - if overrides is not None: - cfg_train.model = OmegaConf.merge(cfg_train.model, overrides) - cfg_train.optim = OmegaConf.load(CONFIGS_DIR / "optim" / "adamw.yaml") - cfg_train.scheduler = OmegaConf.load(CONFIGS_DIR / "scheduler" / "cosine_epoch.yaml") - cfg_train.model.encoder.input_dim = 8 - cfg_train.model.task = cfg_train.task - cfg_train.model.optim = cfg_train.optim - cfg_train.model.scheduler = cfg_train.scheduler - cfg_train.trainer.fast_dev_run = True - cfg_train.paths.output_dir = str(tmp_path) - cfg_train.paths.log_dir = str(tmp_path) - - HydraConfig().set_config(cfg_train) - metric_dict, _ = train(cfg_train) - assert metric_dict is not None - - -def test_survival_task_config_validation() -> None: - from grass_mil.models.training.builders import validate_task_config - - validate_task_config( - {"target_type": "survival", "target_columns": ["time", "event"], "loss": "survival_coxsgd"} - ) - - with pytest.raises(ValueError, match="exactly two target columns"): - validate_task_config({"target_type": "survival", "target_columns": ["time"]}) - - with pytest.raises(ValueError, match="survival_coxsgd"): - validate_task_config( - { - "target_type": "survival", - "target_columns": ["time", "event"], - "loss": "categorical_bce", - } - ) diff --git a/tools/update_repository_map.py b/tools/update_repository_map.py new file mode 100644 index 0000000..f721126 --- /dev/null +++ b/tools/update_repository_map.py @@ -0,0 +1,152 @@ +"""Regenerate docs/repository_map.md from `git ls-files`. + + python tools/update_repository_map.py + +Descriptions come from a small table of known files, else from a module's first +docstring line, a YAML file's first comment, or a Markdown file's title. +""" + +from __future__ import annotations + +import ast +import re +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + +KNOWN = { + ".project-root": "Root marker used by `rootutils.setup_root(...)`.", + ".gitignore": "Ignore rules for generated artefacts, environments, logs, data and vignette outputs.", + ".pre-commit-config.yaml": "Code-quality hooks (ruff lint + format, file hygiene checks).", + "LICENSE": "MIT license.", + "README.md": "Landing page: method summary, install, typical usage, workflow table, citation.", + "CITATION.cff": "Machine-readable citation (preprint DOI 10.64898/2026.10.08.757722).", + "CHANGELOG.md": "Release notes.", + "Makefile": "Convenience targets: `test`, `format`, `clean`, `clean-logs`.", + "pyproject.toml": "Package metadata, dependencies, extras, console entry points (`grass-mil-*`), pytest/ruff configuration.", + "environment.yaml": "Conda environment alternative for local development.", + ".github/PULL_REQUEST_TEMPLATE.md": "Pull-request template.", + ".github/codecov.yml": "Codecov configuration.", + ".github/dependabot.yml": "Dependabot update policy.", + ".github/release-drafter.yml": "Release drafter configuration.", + ".github/workflows/test.yml": "CI: pytest on Linux and macOS, coverage, install-from-source smoke test.", + ".github/workflows/code-quality-main.yaml": "CI: pre-commit on every push to main.", + ".github/workflows/code-quality-pr.yaml": "CI: pre-commit on changed files of a pull request.", + ".github/workflows/release-drafter.yml": "CI: draft release notes from merged pull requests.", + ".vscode/settings.json": "Editor settings.", + "tools/update_repository_map.py": "Regenerates this map.", + "vignettes/README.md": "Index of the worked examples on the public MIBI-TOF dataset.", + "vignettes/01_anndata_to_spatial_graphs.ipynb": "Vignette 1: AnnData -> manifest -> cellular graphs.", + "vignettes/02_pretraining_and_niches.ipynb": "Vignette 2: BGRL pretraining, fine-tuning, instance export, interpretability report.", + "vignettes/03_nested_cv_niches_and_metaclusters.ipynb": "Vignette 3: nested CV, cell extraction, niches, metaclusters, scores, spatial statistics.", + "src/grass_mil/configs/local/.gitkeep": "Placeholder for untracked machine-local configs.", + "tests/conftest.py": "Shared pytest fixtures (Hydra configs on a synthetic datamodule).", + "tests/helpers/synthetic_cohort.py": "Synthetic cohort on disk (regions, labels, folds) with a planted niche, used by the phase tests.", + "tests/helpers/synthetic_datamodule.py": "In-memory bag datamodule for fast training tests.", +} + +GROUPS = [ + ("Root metadata and tooling", lambda f: "/" not in f), + ("CI and contribution files", lambda f: f.startswith((".github/", ".vscode/"))), + ("Maintenance tools", lambda f: f.startswith("tools/")), + ( + "Package: entry points and shared modules", + lambda f: f.startswith("src/grass_mil/") and f.count("/") == 2, + ), + ("Package: data pipeline", lambda f: f.startswith("src/grass_mil/data/")), + ("Package: models", lambda f: f.startswith("src/grass_mil/models/")), + ("Package: inference", lambda f: f.startswith("src/grass_mil/inference/")), + ("Package: evaluation statistics", lambda f: f.startswith("src/grass_mil/evaluation/")), + ( + "Package: interpretability (niches, metaclusters, report)", + lambda f: f.startswith("src/grass_mil/interpretability/"), + ), + ( + "Package: classical spatial statistics", + lambda f: f.startswith("src/grass_mil/spatial_stats/"), + ), + ("Package: utilities", lambda f: f.startswith("src/grass_mil/utils/")), + ("Hydra configs", lambda f: f.startswith("src/grass_mil/configs/")), + ("Documentation", lambda f: f.startswith("docs/")), + ("Vignettes", lambda f: f.startswith("vignettes/")), + ("Tests", lambda f: f.startswith("tests/")), +] + + +def describe(path: str) -> str: + if path in KNOWN: + return KNOWN[path] + p = ROOT / path + if p.suffix == ".py": + try: + doc = ast.get_docstring(ast.parse(p.read_text())) + except Exception: + doc = None + if doc: + first = doc.strip().splitlines()[0].strip() + return first if first.endswith((".", ":")) else first + "." + if p.name == "__init__.py": + return "Package marker / re-exports." + if path.startswith("tests/"): + return ( + "Tests for " + + re.sub(r"^tests?[_/]|test_|\.py$", "", p.stem).replace("_", " ") + + "." + ) + return "Module." + if p.suffix in {".yaml", ".yml"}: + for line in p.read_text().splitlines(): + line = line.strip() + if line.startswith("#") and not line.startswith("# @package"): + text = line.lstrip("# ").rstrip() + return text if text.endswith((".", ":")) else text + "." + if line and not line.startswith("#"): + break + return "Hydra config: `" + p.stem + "`." + if p.suffix == ".md": + return p.read_text().strip().splitlines()[0].lstrip("# ").strip() + "." + if p.suffix == ".ipynb": + return "Notebook." + return "Tracked file." + + +def main() -> None: + files = subprocess.run( + ["git", "ls-files"], capture_output=True, text=True, cwd=ROOT + ).stdout.split() + files = [f for f in files if (ROOT / f).exists()] + out = [ + "# Repository Map", + "", + "Every tracked file (`git ls-files`) with a one-line purpose. Regenerate with", + "`python tools/update_repository_map.py` after adding files.", + "", + ] + seen: set[str] = set() + for title, pred in GROUPS: + members = [f for f in files if f not in seen and pred(f)] + if not members: + continue + out += [f"## {title}", ""] + for f in sorted(members): + seen.add(f) + out.append(f"- `{f}`: {describe(f)}") + out.append("") + rest = [f for f in files if f not in seen] + if rest: + out += ["## Other", ""] + [f"- `{f}`: {describe(f)}" for f in sorted(rest)] + [""] + out += [ + "## Generated at runtime (untracked)", + "", + "- `logs/`: Hydra run directories (checkpoints, `metrics_per_epoch.jsonl`, predictions, caches, reports).", + "- `data/`: raw manifests and processed graphs (`processed_index.json`, `metadata.json`, `graphs/`).", + "- `vignettes/vignette_work/`: data, graphs and runs produced by the vignettes.", + "", + ] + (ROOT / "docs/repository_map.md").write_text("\n".join(out)) + print(len(files), "files mapped") + + +if __name__ == "__main__": + main() diff --git a/vignettes/01_anndata_to_spatial_graphs.ipynb b/vignettes/01_anndata_to_spatial_graphs.ipynb index a51e3f8..54b6492 100644 --- a/vignettes/01_anndata_to_spatial_graphs.ipynb +++ b/vignettes/01_anndata_to_spatial_graphs.ipynb @@ -39,10 +39,10 @@ "id": "9a63283cbaf04dbcab1f6479b197f3a8", "metadata": { "execution": { - "iopub.execute_input": "2026-08-17T07:36:42.466918Z", - "iopub.status.busy": "2026-08-17T07:36:42.466817Z", - "iopub.status.idle": "2026-08-17T07:36:44.219648Z", - "shell.execute_reply": "2026-08-17T07:36:44.219175Z" + "iopub.execute_input": "2026-10-13T17:58:23.292933Z", + "iopub.status.busy": "2026-10-13T17:58:23.292845Z", + "iopub.status.idle": "2026-10-13T17:58:27.234464Z", + "shell.execute_reply": "2026-10-13T17:58:27.233614Z" } }, "outputs": [ @@ -50,8 +50,16 @@ "name": "stderr", "output_type": "stream", "text": [ - "/tmp/ci312/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", - " from .autonotebook import tqdm as notebook_tqdm\n" + "/local/home/lrabuzin/anaconda3/envs/spatial_leomed/lib/python3.11/site-packages/dask/dataframe/__init__.py:31: FutureWarning: The legacy Dask DataFrame implementation is deprecated and will be removed in a future version. Set the configuration option `dataframe.query-planning` to `True` or None to enable the new Dask Dataframe implementation and silence this warning.\n", + " warnings.warn(\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/local/home/lrabuzin/anaconda3/envs/spatial_leomed/lib/python3.11/site-packages/pyproj/__init__.py:91: UserWarning: Valid PROJ data directory not found. Either set the path using the environmental variable PROJ_DATA (PROJ 9.1+) | PROJ_LIB (PROJ<9.1) or with `pyproj.datadir.set_data_dir`.\n", + " warnings.warn(str(err))\n" ] }, { @@ -60,6 +68,14 @@ "text": [ "working directory: vignette_work/\n" ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/local/home/lrabuzin/anaconda3/envs/spatial_leomed/lib/python3.11/site-packages/anndata/utils.py:429: FutureWarning: Importing read_text from `anndata` is deprecated. Import anndata.io.read_text instead.\n", + " warnings.warn(msg, FutureWarning)\n" + ] } ], "source": [ @@ -94,10 +110,10 @@ "id": "72eea5119410473aa328ad9291626812", "metadata": { "execution": { - "iopub.execute_input": "2026-08-17T07:36:44.221227Z", - "iopub.status.busy": "2026-08-17T07:36:44.221016Z", - "iopub.status.idle": "2026-08-17T07:36:44.250807Z", - "shell.execute_reply": "2026-08-17T07:36:44.250510Z" + "iopub.execute_input": "2026-10-13T17:58:27.235989Z", + "iopub.status.busy": "2026-10-13T17:58:27.235740Z", + "iopub.status.idle": "2026-10-13T17:58:27.262442Z", + "shell.execute_reply": "2026-10-13T17:58:27.262138Z" } }, "outputs": [ @@ -111,7 +127,6 @@ " uns: 'Cluster_colors', 'batch_colors', 'neighbors', 'spatial', 'umap'\n", " obsm: 'X_scanorama', 'X_umap', 'spatial'\n", " obsp: 'connectivities', 'distances'\n", - " layers: None (.X)\n", "\n", "images : ['point16', 'point23', 'point8']\n", "donors : ['21d7', '90de']\n", @@ -120,7 +135,8 @@ } ], "source": [ - "adata = sq.datasets.mibitof()\n", + "# Downloaded once into the work directory (~20 MB); reused on later runs.\n", + "adata = sq.datasets.mibitof(path=WORK / \"mibitof.h5ad\")\n", "print(adata)\n", "print()\n", "print(\"images :\", sorted(adata.obs[\"library_id\"].unique()))\n", @@ -153,10 +169,10 @@ "id": "10185d26023b46108eb7d9f57d49d2b3", "metadata": { "execution": { - "iopub.execute_input": "2026-08-17T07:36:44.252228Z", - "iopub.status.busy": "2026-08-17T07:36:44.252144Z", - "iopub.status.idle": "2026-08-17T07:36:44.381778Z", - "shell.execute_reply": "2026-08-17T07:36:44.381481Z" + "iopub.execute_input": "2026-10-13T17:58:27.264067Z", + "iopub.status.busy": "2026-10-13T17:58:27.263979Z", + "iopub.status.idle": "2026-10-13T17:58:27.392847Z", + "shell.execute_reply": "2026-10-13T17:58:27.392394Z" } }, "outputs": [ @@ -284,7 +300,12 @@ "- **`tiling.tile_size_um=100`** gives ~16 patches per image at this scale.\n", "- **`min_cells`** drops patches too sparse to form a useful graph.\n", "\n", - "Run it through the CLI so the exact command is reusable outside a notebook.\n" + "Run it through the CLI so the exact command is reusable outside a notebook.\n", + "\n", + "The graph is a Delaunay triangulation of the cell centroids; every edge carries\n", + "the bounded log edge length `clip((ln d - 2)/3, 0, 1)` that the GRASS-MIL encoder\n", + "conditions on (`log_distance`) and a `neighbor` flag for edges shorter than\n", + "`neighbor_cutoff_um`. `+trainer.fast_dev_run=true` only runs the precompute.\n" ] }, { @@ -293,10 +314,10 @@ "id": "7623eae2785240b9bd12b16a66d81610", "metadata": { "execution": { - "iopub.execute_input": "2026-08-17T07:36:44.383728Z", - "iopub.status.busy": "2026-08-17T07:36:44.383555Z", - "iopub.status.idle": "2026-08-17T07:36:48.550748Z", - "shell.execute_reply": "2026-08-17T07:36:48.550314Z" + "iopub.execute_input": "2026-10-13T17:58:27.394571Z", + "iopub.status.busy": "2026-10-13T17:58:27.394474Z", + "iopub.status.idle": "2026-10-13T17:58:31.656297Z", + "shell.execute_reply": "2026-10-13T17:58:31.655625Z" } }, "outputs": [ @@ -319,6 +340,8 @@ " \"grass_mil.train\",\n", " \"task=finetune_mean\",\n", " \"model=supervised_module\",\n", + " \"data.graph_builder.kwargs.edge_features=[log_distance,neighbor]\",\n", + " \"data.graph_builder.kwargs.neighbor_cutoff_um=20\",\n", " \"data=spatial_omics\",\n", " \"trainer=cpu\",\n", " f\"data.raw_manifest_path={RAW}/manifest.csv\",\n", @@ -363,10 +386,10 @@ "id": "b118ea5561624da68c537baed56e602f", "metadata": { "execution": { - "iopub.execute_input": "2026-08-17T07:36:48.552816Z", - "iopub.status.busy": "2026-08-17T07:36:48.552718Z", - "iopub.status.idle": "2026-08-17T07:36:48.555784Z", - "shell.execute_reply": "2026-08-17T07:36:48.555545Z" + "iopub.execute_input": "2026-10-13T17:58:31.657557Z", + "iopub.status.busy": "2026-10-13T17:58:31.657469Z", + "iopub.status.idle": "2026-10-13T17:58:31.660598Z", + "shell.execute_reply": "2026-10-13T17:58:31.660146Z" } }, "outputs": [ @@ -432,10 +455,10 @@ "id": "59bbdb311c014d738909a11f9e486628", "metadata": { "execution": { - "iopub.execute_input": "2026-08-17T07:36:48.557659Z", - "iopub.status.busy": "2026-08-17T07:36:48.557573Z", - "iopub.status.idle": "2026-08-17T07:36:50.032400Z", - "shell.execute_reply": "2026-08-17T07:36:50.031507Z" + "iopub.execute_input": "2026-10-13T17:58:31.661948Z", + "iopub.status.busy": "2026-10-13T17:58:31.661868Z", + "iopub.status.idle": "2026-10-13T17:58:32.345987Z", + "shell.execute_reply": "2026-10-13T17:58:32.345463Z" } }, "outputs": [ @@ -449,6 +472,7 @@ " pos=[59, 2],\n", " edge_attr=[322, 2],\n", " edge_attr_names=[2],\n", + " molecular_feature_names=[36],\n", " categorical_codes=[59, 1],\n", " categorical_labels=[1],\n", " categorical_slices={ cell_type=0 },\n", @@ -483,16 +507,16 @@ "id": "b43b363d81ae4b689946ece5c682cd59", "metadata": { "execution": { - "iopub.execute_input": "2026-08-17T07:36:50.034039Z", - "iopub.status.busy": "2026-08-17T07:36:50.033926Z", - "iopub.status.idle": "2026-08-17T07:36:50.152988Z", - "shell.execute_reply": "2026-08-17T07:36:50.152643Z" + "iopub.execute_input": "2026-10-13T17:58:32.347416Z", + "iopub.status.busy": "2026-10-13T17:58:32.347329Z", + "iopub.status.idle": "2026-10-13T17:58:32.488605Z", + "shell.execute_reply": "2026-10-13T17:58:32.487617Z" } }, "outputs": [ { "data": { - "image/png": "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/vdd2eeqVaugUCjw/PPPL/h88W9aj2Z/11Y5+OCDkcvlllyrzY6r3fE1cy0TmocIpxXG2rVrsWXLFrz11lt48803cdppp+H111/H5ZdfLi7zyU9+Eul0Gl/5ylcQiUTwn//8B0cddRT23nvvBduy2WwYGxvDn//8Z0QiEdA0DYZhAABHH300Dj/8cJx11lm49tprEQqF8Pbbb+Oaa67B17/+9b4f04knnog999wTp59+On7/+98jFovhvvvuw2c/+1msX78ep556alfG0uicAO/W8aFpWkybF/6evxxFUfjJT36CZ555Bueddx5CoRBef/11bN68GalUqq36RAA5V60wjPeKgFarxRVXXIF77rkHV1xxBZLJJP75z3/ic5/7HPbee29s3ry5a/tq5viOPvpoHHnkkTj11FNx3XXX4e2330YsFsPf//53nHPOOQvOeTNYrVYcd9xx+NnPfoa//OUviMViuO222/Dvf/+7KZdos79rq2zevBm77bYbTj31VDz00EOIxWK488478fjjj0Or1Ta9nVbH18y1TGiBgYSkE/qOkDFx/fXX87fddhu/yy678BaLhd977735+++/f8nyN954I//e976Xt9ls/Pr16/lt27bx27Zt47VaLf/888+Lyz3wwAP8nnvuyZvNZl6r1fLnnHOO+F21WuV/9KMf8evXr+ctFgu/44478meeeSYfiUR4nuf5N998k9dqtfwvfvGLJftfs2YNf+qpp3b1mDKZDH/eeefxa9as4U0mE7969Wr+y1/+Mp9Op8Vl4vE4r9VqF2Q/tTrORufknHPO4bVabc1/QubQfB566CH+oIMO4u12O+90OvmPf/zj/FNPPdXwvJBz1d9zJYd7pR7btm3j99prL95isfBjY2P82WefveA37hbLHR/P8zzLsvw111zD77PPPrzD4eC9Xi+/33778Vu3buXz+TzP87WvuVqf8TzPJ5NJfvPmzbzL5eJHRkb4L37xizxN07zRaOQvuuiiZddv5ndt9LvUIxqN8qeccgrv8Xh4j8fDf/azn+XT6TSvVCr5r3zlK+Jyy227mfG1ci336hobRohwWiHMv4GGhWE8pl5BzlXzkHNF6DdCyr9Q6qNbkGu5NxBXHYFAIBAIA2Tbtm0AgAMOOGCwAyE0BcmqIxBkTKVSaZglRFFUS7ETw8xKOFcr4Rjlzje+8Q2s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9a9/reHKF49hXXVPP/008fl85PDhw+SOO+4g55xzDvH5fNv+PpTJyeVyJBKJkAsuuID86Ec/In/zN39D3G43+fSnPy095oknniAej4ecd9555I477iDvete7yNLSEnn22Wc1XPn88xd/8RfE6XSSz3zmM+Sf//mfyZ//+Z8Tk8lErr/+eukxjzzyCHE6neR973sfueOOO8gf/dEfkVAoRNLptIYrnz9++ctfkrvvvpscPnyYJBIJ6fze7/elx5xzzjkkkUiQr33ta+T6668nXq+X/O///b+3vc44j5kUhhDaWiQ3hBDceOON+O53v4vBYIDXve51uOiii2Ay0cyonLz5zW8e6kb9lre8BZdddpn081NPPYW//Mu/xNNPP421tTVcfvnleNGLXqTmUheeT3ziE2i1WvjMZz6z7fZf//rX+MxnPoPf/OY3OPLII3HFFVfguOOO02iVi8P6+jr+8i//Eo8++ijC4TDe/va3441vfOO2xzz66KO49tpr8cwzz+AFL3gBPvShD+GYY47RaMWLw7e+9S185zvfQT6fRzQaxTvf+U6ceeaZ2x7z8MMP46/+6q+QSqVw9NFH48Mf/jCOPPJIjVY8n7z//e8fOo3gjjvugN/vB7DZFPT5z38eP/rRj2AymfCWt7wF55133rY6vnEeMylUOFEoFAqFQqGMCa1xolAoFAqFQhkTKpwoFAqFQqFQxoQKJwqFQqFQKJQxocKJQqFQKBQKZUyocKJQKBQKhUIZEyqcKBQKhUKhUMaECicKhUKhUCiUMaHCiUKhHAgGgwG++c1vguM4xd/rH/7hH9ButxV/HwqFoj5UOFEolAPBF77wBdxyyy2qDHf+wQ9+gKuvvlrx96FQKOpDncMpFMrC0+l0EI/H8Y//+I94+ctfrvj7Pf744zjxxBORSqW2DYClUCjzD404USiUueKpp57CN77xDfT7fem2breLb3zjG3jmmWeGPufrX/86gsHgNtF0++23I5/Pb3vcnXfeiSeffFL6+Tvf+Q5SqRQ2NjbwT//0T7j77rshCAIAIJlM4nvf+x4eeOCBXe937LHH4sUvfjFuvPHGmX5XCoWiP2jEiUKhzBWNRgO//du/jTe/+c249tprAQAXXngh/uVf/gUPPvggnE7nrue86U1vQiQSwZe+9CXpNoZh8OMf/xi/93u/J9123HHH4aKLLsJFF10EAAgGgzj++OORyWTwohe9CPfeey9OOeUUnH766fjqV7+K4447DnfffTf+5E/+BH/913+97T2vvPJK3H333bjnnnsUOAoUCkUrTFovgEKhUCbB4/Hgtttuw+mnn44zzzwTvV4PN9xwA+69996hogkAHnjgAXzoQx+a6v0GgwEeffRR2Gw2/OIXv8BLX/pS9Pt9PProo7BYLPjpT3+K1772tfjoRz+KcDgsPe+EE07AZz/72anek0Kh6BcqnCgUytzxile8Ah/72Mdw7rnnYjAY4BOf+ARe+tKXjnx8qVTC0tLSVO91+PBh2Gw2AMApp5wCi8WC8847DxaLBQDw8pe/HIIg4KmnntomnJaWltDr9dBsNuF2u6d6bwqFoj9ojROFQplLPvKRj6DVasFsNuMjH/nIno91uVxT2wNsFVwMw8BsNm+7zWq1AgBYlt32vHa7DYPBAIfDMdX7UigUfUKFE4VCmUuuuuoqLC0todls4vrrr9/zscccc8yuwnGGYaRCb5Gd4mcWnnnmGRx11FEwGo2yvSaFQtEeKpwoFMrc8a//+q/467/+a3z961/H3/7t3+Lyyy/H448/PvLxr33ta3Hvvfduuy0ajeKpp56Sfn766aexvr4u2xrvvffebYXnFAplMaA1ThQKZa6oVCo4fPgwPvKRj+C0007Daaedhh/+8If44z/+Y/zsZz8banB5/vnn45prrkEqlUIikQCwWbv0iU98AhzHgRCCG264AXa7XZY11ut1/OhHP8J9990ny+tRKBT9QCNOFAplrrjzzjvxB3/wB7jqqquk26677jocf/zx+Ld/+7ehz1lbW8PZZ5+Nz33uc9Jtf/EXf4FrrrkG//3f/41yuYxvfetbeP/7349jjjlGesyb3/xmrK2tbXutt771rYjFYttue/vb376tMPz666/H7/7u7+Kkk06a6XelUCj6g/o4USiUA0E+n8eVV16J6667TvGxKxdddBEuueQSHH300Yq+D4VCUR8qnCgUCoVCoVDGhKbqKBQKhUKhUMaECicKhUKhUCiUMaHCiUKhUCgUCmVMqHCiUCgUCoVCGRMqnCgUCoVCoVDGhAonCoVCoVAolDGhwolCoVAoFAplTKhwolAoFAqFQhkTKpwoFAqFQqFQxoQKJwqFQqFQKJQxocKJQqFQKBQKZUz+PweYH26pHUDMAAAAAElFTkSuQmCC", + "image/png": "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Ga699lrstddegg/+oIMOgslkwnnnnYfrrrsOcrkcDz/88LibL/C/p9q1a9fimGOOgVQqxZ577olDDjkEZ511Fm688UYMDg7i+OOPh1KpxEcffQSNRlOWVHIeqVSKRYsW4bLLLgPHcVi7di2CwWBGYc5bbrkFixYtwhFHHIErrrgCCoUCd999Nz799FM88sgjeT9N5kKv16OzsxNbt27FwoULYTabYbVahZT75557DpFIBKFQCMBI3aInnngCAHDssccKlqZf/OIXeO655/Cd73wHa9asgcFgwP3334+PP/4Yjz/+eMHjstvtOPHEE7FmzRq0tbVh8+bN2LZtG9auXSts89JLL8VDDz2E4447DjfccAM6Ozvxl7/8BXfffTfOP//8sghLYOR8efTRR/HYY49hl112gUqlEs6h999/XxCLwWAQhBDh+Oy7777Ck/cPfvAD3HPPPbjgggvg9Xoxa9YsvPDCC7jrrrtwwQUXZDyh54NCocBvfvMbhMNh7LvvvnjzzTdx44034phjjsG3v/1tAMCiRYugUChw+umn48orr0Q8Hsfvf/97BAKBrPv45JNP4ve//z322WcfSCQSLFiwAKeffjoefPBBnHfeefjyyy9xxBFHgOM4vPPOO5g5cya+//3vF3tYsyKVSnHrrbfirLPOwsqVK3H66afj3//+N6688kosWrQI3/nOd8q2rWnTpuHqq6/GL37xC8RiMZx++uloamrC559/Dq/Xi+uvvx46nQ7r1q3D8uXL4ff7sXTpUthsNng8Hnz88cfweDwFPWgxDINly5bhD3/4A3bddVfstddeePfddwXRm4s99tgDu+66K6666ioQQmA2m/Hss89i27ZtpRwGAMA111yDZ599FgsXLsQ111wDtVqN9evXC/Fb+VgbCznfgBGr0vnnn4/u7m7stttu+Otf/4r77rsP559/fkb8KKVIahWVTqk9fHbIBx98QE444QSi0+mIXq8np59+OhkcHMxY9s033yQHHngg0Wg0pLm5mZx77rnkww8/HJfFkkgkyLnnnkuam5sJwzAZWSMsy5Lf/va3ZM6cOUShUJCmpiZy4IEHkmeffVb4fmdnJznuuOPGjfWwww6bNPuKzxJZu3Ytuf7664nD4SAKhYLMnz+f/O1vfxu3/Ouvv06OPPJIotVqiVqtJgcccEDGWAiZOKtOq9VOeDxH88ILL5D58+cTpVJJAJDly5dn7CuArH9js30++eQTctxxxxG9Xk9UKlXWseYDf3yfeOIJMnv2bKJQKMi0adPIbbfdNm7Zrq4ucsYZZxCLxULkcjnZfffdya9+9SvCsmzGcpggY+m9997LWC7bsdy5cydZvHgx0ev1BEBGlhSf4Zbtb2zmlM/nIytXriQtLS1ELpeT3XbbLetYJ4P/bf/5z3+Sww8/nKjVamI2m8n5559PwuFwxrLPPvss2WuvvYhKpSLt7e3kxz/+MXnuuefG7aPf7ydLly4lRqNRuCZ4YrEY+fnPf05mzJhBFAoFsVgs5MgjjyRvvvlmxvFdtWrVuLF2dnZmnE/5smXLFrLnnnsShUJBWltbycUXX5yRXVpOHnroIbLvvvsSlUpFdDodmT9//rjf7tVXXyXHHXccMZvNRC6Xk/b2dnLccceRP/7xj8Iy+WTVEULI8PAwOffcc0lLSwvRarXkhBNOIDt37swrq+7zzz8nixYtEjI0Tz31VNLd3T3uu/x1PjpLbjJef/11sv/++xOlUklaW1vJj3/8Y7J27VoCgAwNDQnLTTT/EZL/+XbYYYeR2bNnk1deeYUsWLCAKJVK0tbWRq6++mqSSqWE5fj58le/+tW4bY3dZ0omDCHfBAJQphxr1qzB9ddfD4/HI6qYlWLZuXMnpk+fjl/96le44ooraj0cUTJt2jTMmTOn4OKJU4UVK1YIrisKpZIsXrwYO3fuzCsDsxAOP/xweL3estflovwP6qqjUCgUCqWCXHbZZZg/fz46Ojrg9/vx8MMPY9u2beOqqFPqAyqcKJQGgGVZ5DIeMwyTUatlqsFxXM66Q8BIIH69kk6nc34ukUiqkrlHyQ7Lsvj5z3+OgYEBMAyDWbNmYdOmTRmV9Cn1A3XVUSgNwLRp03IWrDvssMMy+u1NNVasWDFpi5p6nQp5F3Uurrvuukl7tVEolPygwolCaQA++eSTcfVrRqPX67H77rtXcUTiYufOneOatY5lwYIFVRpNeUkmk+NS8Mdit9tzlg+gUCj5Q4UThUKhUCgUSp5QpzeFQqFQKBRKntRvNGQZ4TgOfX190Ov1ZSl8SKFQKBQKpX4ghCAUCuXVP5IKJ4z035qonxKFQqFQKJSpQU9PT87m1AAVTgBGAmeBkQNmMBhqPBoKhUKhUCjVJBgMoqOjQ9ADuaDCCf9roGgwGKhwolAoFAplipJPuA4NDqdQKBQKhULJE2pxolAoFAqlRFiWRSqVqvUwKHkgl8tL6qRAhROFQqFQKCUQDofhcrnqtvr8VINhGDgcDuh0uqK+T4UThUKhUChFwrIsXC4XNBoNmpubaUkbkUMIgcfjgcvlwowZM4qyPFHhRKFQKBRKkaRSKRBC0NzcDLVaXevhUPKgubkZO3fuRCqVKko40eBwCoVCoVBKhFqa6odSfysqnCgUCoVCoVDyhAonCoVCoVDqGKvVWtPtv/LKK3j33XeF1ytWrMCf//znGo6ostAYJwqFQqFQqkSK5fCnD1zo8kfRadbgu/s4IJfWtw3jlVdegdVqxX777VfyuliWLalUQDWo71+LQqFQKJQ6IcVyWHb/O7jqyU9w32tf46onP8Gy+99BiuXKsv5XXnkFRx11FL773e/iW9/6Fm6++WasX78ee++9N/bbbz94vV4AwOGHH44rrrgC3/72t7Hnnnviww8/xHHHHYdvfetbuPPOOydcfywWw1lnnYW5c+div/32w/bt29HT04P169fjl7/8JebNm4dPPvkEALBt2zYccMABmDFjBl599VUAQDqdxiWXXIL99tsP8+bNw9NPPw0A2LBhA04//XQcd9xxOP3008tyLCoJtThRKBQKhVIF/vSBC+/s8AMA0txIzad3dvjx5IcufG9fZ1m2sX37dnzxxRfQaDSYPn06rr76anz44Ye46qqrsGnTJlx66aUAAK1WizfeeAM33XQTvve97+G9994Dx3GYPXs2Lrzwwqzrvuuuu6DX6/HJJ5/g7bffxvLly/Hxxx/jvPPOg9Vqzfje0NAQ3n77bbz00ku44YYb8OKLL+L+++/HtGnT8Lvf/Q7BYBD7778/jjnmGADAu+++i48++qgu2p5R4UShUCgUShXo8kchkzCCaAIAmYRBly9atm0ceOCBaG5uBgA4HA5BmOy555545513hOVOPPFEAMDcuXOxYMECGI1GACNN7wOBAEwm07h1v/HGG7jyyisBAAcccABisRiGh4ezjoNf/z777IOdO3cCGLFCffbZZ9i4cSMAIBKJoLe3FwBw9NFH14VoAqhwolAoFAqlKnSaNRmiCRixPHVaNGXbhlKpFP4vkUiE1xKJBCzLjltu9DLZlssFIWTC1H5+nVKpVFgfIQT33nsvDj300IxlX3vtNWg0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7nPpe6ECV155JV7ykpfgN3/zN/fcTo+7tnzta1/Dueeei1e+8pUIh8M4++yzceONNyr30+OvHc961rPwf//v/8WDDz4IAPjxj3+Mu+66Cy9+8YsB0GOvF5Mc53vuuQfD4XDPY+LxOM4666y53ws65FdlyuUyRFFEJBLZc3skEkE+n1/QqlYfQgje/OY341nPehbOOussAFCO96j3IpVK6b7GVeLWW2/Fj370I/zgBz84cB897try0EMP4SMf+Qje/OY3453vfCfuvvtuvPGNb4TNZsPFF19Mj7+GvO1tb0Oj0cDjHvc4mEwmiKKI973vfXj1q18NgH729WKS45zP52G1WuHz+Q48Zt5rMRVOGsEwzJ6fCSEHbqOox1VXXYWf/OQnuOuuuw7cR98LdclkMnjTm96E2267DRzHjX0cPe7aIEkSzj33XFx33XUAgLPPPhsPPPAAPvKRj+Diiy9WHkePv/p84QtfwGc+8xl87nOfw6/92q/hvvvuwzXXXIN4PI5LLrlEeRw99vowy3FW472gqTqVCQaDMJlMBxRtsVg8oI4p6nD11Vfja1/7Gu644w6sr68rt0ejUQCg74XK3HPPPSgWizjnnHNgNpthNpvxzW9+E3/7t38Ls9msHFt63LUhFovhCU94wp7bHv/4xyOdTgOgn3st+eM//mO8/e1vx+/93u/hiU98Ii666CJce+21uP766wHQY68XkxznaDSKwWCAWq029jGzQoWTylitVpxzzjm4/fbb99x+++234xnPeMaCVrWaEEJw1VVX4Stf+Qr+7d/+DSdPntxz/8mTJxGNRve8F4PBAN/85jfpezEHL3zhC3H//ffjvvvuU/6de+65uOCCC3DffffhtNNOo8ddQ575zGcesN148MEHsbm5CYB+7rWk2+2CZfdeNk0mk2JHQI+9PkxynM855xxYLJY9j8nlcvjpT386/3sxV2k5ZSSyHcFNN91Efvazn5FrrrmGOJ1O8vDDDy96aSvFH/7hHxKv10v+/d//neRyOeVft9tVHvP+97+feL1e8pWvfIXcf//95NWvfjVtDdaA3V11hNDjriV33303MZvN5H3vex/5xS9+QT772c8Sh8NBPvOZzyiPocdfGy655BKSSCQUO4KvfOUrJBgMkre+9a3KY+ixV4dWq0Xuvfdecu+99xIA5G/+5m/Ivffeq9j6THKcL7/8crK+vk7+9V//lfzoRz8iL3jBC6gdgZH58Ic/TDY3N4nVaiW//uu/rrTIU9QDwMh/n/zkJ5XHSJJE/uzP/oxEo1Fis9nIc57zHHL//fcvbtEryn7hRI+7tvzv//2/yVlnnUVsNht53OMeRz72sY/tuZ8ef21oNpvkTW96E0kmk4TjOHLaaaeRd73rXaTf7yuPocdeHe64446R5/dLLrmEEDLZce71euSqq64ifr+f2O128tKXvpSk0+m518YQQsh8MSsKhUKhUCiU4wGtcaJQKBQKhUKZECqcKBQKhUKhUCaECicKhUKhUCiUCaHCiUKhUCgUCmVCqHCiUCgUCoVCmRAqnCgUCoVCoVAmhAonCoVCoVAolAmhwolCoVAoFAplQqhwolAox4KbbroJ5513nuav88///M84++yzlfllFApltaDCiUKhrDz9fh/vfve78ad/+qeav9ZLX/pSMAyDz33uc5q/FoVC0R8qnCgUysrz5S9/GS6XC89+9rN1eb3Xvva1+Lu/+ztdXotCoegLFU4UCmVpKJVKiEajuO6665Tbvv/978NqteK2224b+3u33norXvayl+257XnPex6uueaaPbe9/OUvx6WXXqr8fOLECbz3ve/FxRdfDJfLhc3NTfzTP/0TSqUSfud3fgculwtPfOIT8cMf/nDP87zsZS/D3XffjYceemj2P5ZCoRgSKpwoFMrSEAqF8IlPfALvec978MMf/hDtdhsXXnghrrjiikPrl+68806ce+65M73mBz7wATzzmc/Evffei5e85CW46KKLcPHFF+PCCy/Ej370I5x++um4+OKLsXte+ubmJsLhMO68886ZXpNCoRgXKpwoFMpS8eIXvxivf/3rccEFF+Dyyy8Hx3F4//vfP/bx9Xod9Xod8Xh85td7wxvegDPOOAPvfve70Wq18NSnPhWvfOUrceaZZ+Jtb3sb/uM//gOFQmHP7yUSCTz88MMzvSaFQjEuVDhRKJSl46/+6q8gCAK++MUv4rOf/Sw4jhv72F6vBwCHPuYwnvSkJyn/H4lEAABPfOITD9xWLBb3/J7dbke3253pNSkUinGhwolCoSwdDz30ELLZLCRJQiqVOvSxgUAADMOgVqvtuZ1l2T3pNQAYDocHft9isSj/zzDM2Nv22w9Uq1WEQqEJ/hoKhbJMUOFEoVCWisFggAsuuACvetWr8N73vheve93rDqTJdmO1WvGEJzwBP/vZz/bcHgqFkMvllJ9FUcRPf/pTVdbI8zx+9atf4eyzz1bl+SgUinGgwolCoSwV73rXu9BoNPC3f/u3eOtb34rHP/7xeN3rXnfo75x//vm466679tz2ghe8AF//+tfx9a9/Hf/5n/+JK664AvV6XZU1fu9734PNZsPTn/50VZ6PQqEYByqcKBTK0vDv//7v+OAHP4hbbrkFHo8HLMvilltuwV133YWPfOQjY3/v9a9/Pb7xjW+g0Wgot1122WW45JJLcPHFF+O5z30uTp48iec///mqrPPzn/88LrjgAjgcDlWej0KhGAeG7E/yUygUygryu7/7uzj77LPxjne8Q9PXKZVKeNzjHocf/vCHOHnypKavRaFQ9IdGnCgUyrHgL//yL+FyuTR/nVOnTuGGG26goolCWVFoxIlCoVAoFAplQmjEiUKhUCgUCmVCqHCiUCgUCoVCmRAqnCgUCoVCoVAmhAonCoVCoVAolAmhwolCoVAoFAplQqhwolAoFAqFQpkQKpwoFAqFQqFQJoQKJwqFQqFQKJQJocKJQqFQKBQKZUL+fx9IRHgIcdkwAAAAAElFTkSuQmCC", "text/plain": [ "
" ] @@ -557,7 +581,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.11.11" } }, "nbformat": 4, diff --git a/vignettes/02_pretraining_and_niches.ipynb b/vignettes/02_pretraining_and_niches.ipynb index 203c63b..ccc6d2d 100644 --- a/vignettes/02_pretraining_and_niches.ipynb +++ b/vignettes/02_pretraining_and_niches.ipynb @@ -9,8 +9,8 @@ "\n", "Vignette 1 turned three MIBI-TOF images into 44 patch graphs. Here we:\n", "\n", - "1. pretrain a graph encoder with **BGRL**, which needs no labels,\n", - "2. attach a supervised head and export per-instance embeddings,\n", + "1. pretrain a graph encoder with **BGRL** (`experiment=grass_mil_pretrain_a3`), which needs no labels,\n", + "2. fine-tune the MIL model (`regime=mil`) and export per-instance embeddings,\n", "3. run the interpretability report to discover **spatial niches**.\n", "\n", "---\n", @@ -49,10 +49,10 @@ "id": "9a63283cbaf04dbcab1f6479b197f3a8", "metadata": { "execution": { - "iopub.execute_input": "2026-08-17T07:36:51.489375Z", - "iopub.status.busy": "2026-08-17T07:36:51.489276Z", - "iopub.status.idle": "2026-08-17T07:36:51.654305Z", - "shell.execute_reply": "2026-08-17T07:36:51.653762Z" + "iopub.execute_input": "2026-10-13T18:06:17.038316Z", + "iopub.status.busy": "2026-10-13T18:06:17.038225Z", + "iopub.status.idle": "2026-10-13T18:06:17.208367Z", + "shell.execute_reply": "2026-10-13T18:06:17.207510Z" } }, "outputs": [], @@ -86,9 +86,20 @@ " \"data.tiling.stride_um=100\",\n", " \"data.min_cells=20\",\n", " \"data.tiling.min_cells=20\",\n", + " \"data.graph_builder.kwargs.edge_features=[log_distance,neighbor]\",\n", + " \"data.graph_builder.kwargs.neighbor_cutoff_um=20\",\n", + "]\n", + "# Supervised steps split the three images into train / val / test (one each).\n", + "SPLIT_ARGS = [\n", " \"data.split.train_val_test_split=[0.34,0.33,0.33]\",\n", " \"data.split.split_by=sample\",\n", "]\n", + "# Ego-graph settings shared by every model below. The preprint uses depth 4;\n", + "# the demo images are small, so 2 hops keep the CPU runtime short.\n", + "EGO_ARGS = [\n", + " \"data.sampler.runtime.depth=2\",\n", + " \"data.sampler.runtime.subgraph_batch_size=32\",\n", + "]\n", "\n", "\n", "def run(args, tag):\n", @@ -108,13 +119,13 @@ "## 1. BGRL pretraining (no labels)\n", "\n", "BGRL learns node representations by matching two augmented views of the same\n", - "graph. Nothing here uses `donor_label`.\n", - "\n", - "The augmentation preset matters. `bgrl_paper` adds Gaussian noise to a named\n", - "continuous feature (cell size) and requires the reserved 'unassigned'\n", - "cell-type slot. This cohort keeps `cell_size` in `obs`, not in the marker\n", - "matrix, so we use `uniform`, which drops edges and features independently of\n", - "structure and has no feature-name dependency.\n" + "graph. Nothing here uses a label. `experiment=grass_mil_pretrain_a3` is the\n", + "preprint's recipe (the \"A3\" encoder): roots drawn from the union of all\n", + "regions, momentum 0.99 -> 1.0, degree-importance node drop plus Gaussian noise\n", + "on the cell-size feature. This cohort keeps `cell_size` in `obs`, not in the\n", + "marker matrix, so the noise column list is emptied; everything else is the\n", + "default. 200k steps become 300 for a CPU demo, and every tile is used\n", + "(`data.split.train_val_test_split=[1,0,0]` from the experiment preset).\n" ] }, { @@ -123,10 +134,10 @@ "id": "72eea5119410473aa328ad9291626812", "metadata": { "execution": { - "iopub.execute_input": "2026-08-17T07:36:51.655776Z", - "iopub.status.busy": "2026-08-17T07:36:51.655669Z", - "iopub.status.idle": "2026-08-17T07:37:12.353909Z", - "shell.execute_reply": "2026-08-17T07:37:12.353334Z" + "iopub.execute_input": "2026-10-13T18:06:17.210182Z", + "iopub.status.busy": "2026-10-13T18:06:17.210046Z", + "iopub.status.idle": "2026-10-13T18:06:37.834013Z", + "shell.execute_reply": "2026-10-13T18:06:37.833596Z" } }, "outputs": [ @@ -135,7 +146,7 @@ "output_type": "stream", "text": [ "BGRL pretraining: exit 0\n", - "pretrained encoder: logs/train/runs/2026-08-17_09-36-53/checkpoints/last.ckpt\n" + "pretrained encoder: logs/pretrain_a3/runs/2026-10-13_20-06-18/checkpoints/last.ckpt\n" ] } ], @@ -143,19 +154,24 @@ "run(\n", " [\n", " \"grass_mil.train\",\n", - " \"task=pretrain_bgrl\",\n", - " \"model=bgrl_module\",\n", + " \"experiment=grass_mil_pretrain_a3\",\n", " \"trainer=cpu\",\n", - " \"+augmentation@task.augmentation=uniform\",\n", + " \"trainer.precision=32\",\n", " *DATA_ARGS,\n", + " *EGO_ARGS,\n", + " \"data.force_precompute=true\", # all tiles label-free (the preset's [1,0,0] split)\n", + " \"task.total_steps=300\",\n", + " \"task.warmup_steps=30\",\n", + " \"task.lr_warmup.warmup_steps=30\",\n", + " \"task.augmentation.feature_noise_columns=[]\",\n", + " \"callbacks.model_checkpoint.every_n_train_steps=100\",\n", " f\"paths.log_dir={WORK}/logs\",\n", - " \"trainer.max_epochs=10\",\n", " ],\n", " \"BGRL pretraining\",\n", ")\n", "\n", "bgrl_ckpt = sorted(\n", - " glob.glob(f\"{WORK}/logs/train/runs/*/checkpoints/*.ckpt\"), key=os.path.getmtime\n", + " glob.glob(f\"{WORK}/logs/pretrain_a3/runs/*/checkpoints/last.ckpt\"), key=os.path.getmtime\n", ")[-1]\n", "print(\"pretrained encoder:\", os.path.relpath(bgrl_ckpt, WORK))" ] @@ -170,7 +186,9 @@ "Pretraining gave an encoder. Fine-tuning attaches the MIL head and trains it\n", "on a bag-level label: each patch is a bag, the ego-graphs inside it are the\n", "instances, and per-class attention decides which of them the prediction\n", - "leans on.\n", + "leans on. `regime=mil` is the GRASS-MIL default (MLP instance head, attention\n", + "at twice the learning rate, one region per bag, region-level selection); here a\n", + "bag is a tile, so `task.bag_key=patch_id`.\n", "\n", "This cohort has no usable clinical label -- see the note at the top -- so we\n", "assign a **dummy target**: is this patch epithelial-rich? It is derived from\n", @@ -189,10 +207,10 @@ "id": "10185d26023b46108eb7d9f57d49d2b3", "metadata": { "execution": { - "iopub.execute_input": "2026-08-17T07:37:12.355354Z", - "iopub.status.busy": "2026-08-17T07:37:12.355269Z", - "iopub.status.idle": "2026-08-17T07:37:13.774702Z", - "shell.execute_reply": "2026-08-17T07:37:13.774152Z" + "iopub.execute_input": "2026-10-13T18:06:37.835602Z", + "iopub.status.busy": "2026-10-13T18:06:37.835513Z", + "iopub.status.idle": "2026-10-13T18:06:39.239558Z", + "shell.execute_reply": "2026-10-13T18:06:39.238788Z" } }, "outputs": [ @@ -206,14 +224,6 @@ "point23 12 5\n", "point8 16 11\n" ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/tmp/ci312/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", - " from .autonotebook import tqdm as notebook_tqdm\n" - ] } ], "source": [ @@ -254,10 +264,10 @@ "id": "f5a1fa73e5044315a093ec459c9be902", "metadata": { "execution": { - "iopub.execute_input": "2026-08-17T07:37:13.775794Z", - "iopub.status.busy": "2026-08-17T07:37:13.775666Z", - "iopub.status.idle": "2026-08-17T07:37:26.608863Z", - "shell.execute_reply": "2026-08-17T07:37:26.608158Z" + "iopub.execute_input": "2026-10-13T18:06:39.240835Z", + "iopub.status.busy": "2026-10-13T18:06:39.240738Z", + "iopub.status.idle": "2026-10-13T18:06:49.440027Z", + "shell.execute_reply": "2026-10-13T18:06:49.439459Z" } }, "outputs": [ @@ -266,9 +276,9 @@ "output_type": "stream", "text": [ "fine-tuning: exit 0\n", - "fine-tuned checkpoint: logs/train/runs/2026-08-17_09-37-15/checkpoints/last.ckpt\n", - "test/acc: 0.6864105463027954\n", - "test/loss: 0.5490885376930237\n" + "fine-tuned checkpoint: logs/train/runs/2026-10-13_20-06-41/checkpoints/last.ckpt\n", + "test/acc: 0.0\n", + "test/loss_region: 0.8508459329605103\n" ] } ], @@ -279,30 +289,30 @@ " \"data.graph_labels.tasks=[epithelial_rich]\",\n", " \"data.graph_labels.scope=patch\",\n", "]\n", - "SAMPLER_ARGS = [\n", - " \"data.sampler.name=shadow_custom\",\n", - " \"data.sampler.runtime.enabled=true\",\n", - " \"data.sampler.runtime.depth=2\",\n", - " \"data.sampler.runtime.num_neighbors=8\",\n", + "BAG_ARGS = [\n", + " \"task.bag_key=patch_id\", # one tile = one bag\n", + " \"data.sampler.runtime.subsample_count=32\", # ego-graphs per bag per epoch\n", + " \"data.val_sampler.runtime.subsample_count=64\",\n", + " \"task.region_accumulation.hyperbatch_size=4\",\n", "]\n", "\n", "out = run(\n", " [\n", " \"grass_mil.train\",\n", - " # finetune_mil is the published head: per-class attention over the\n", - " # instances of each bag. finetune_mean is the simpler alternative.\n", - " \"task=finetune_mil\",\n", - " \"model=supervised_module\",\n", + " \"regime=mil\",\n", " \"trainer=cpu\",\n", + " \"trainer.precision=32\",\n", " f\"model.init_from_ckpt={bgrl_ckpt}\",\n", - " \"model.encoder_init_map=auto_bgrl_or_identity\",\n", " *DATA_ARGS,\n", - " *SAMPLER_ARGS,\n", + " *SPLIT_ARGS,\n", + " *EGO_ARGS,\n", " *LABEL_ARGS,\n", + " *BAG_ARGS,\n", " # Labels are attached during precompute, so the graphs are rebuilt.\n", " \"data.force_precompute=true\",\n", " f\"paths.log_dir={WORK}/logs\",\n", " \"trainer.max_epochs=8\",\n", + " \"trainer.min_epochs=2\",\n", " ],\n", " \"fine-tuning\",\n", ")\n", @@ -319,7 +329,7 @@ "seen = {}\n", "for line in out.stdout.splitlines():\n", " clean = ansi.sub(\"\", line)\n", - " for key in (\"test/acc\", \"test/loss\"):\n", + " for key in (\"test/auroc\", \"test/bal_acc\", \"test/acc\", \"test/loss_region\"):\n", " if key in clean:\n", " numbers = re.findall(r\"\\d+\\.\\d+\", clean)\n", " if numbers:\n", @@ -361,10 +371,10 @@ "id": "7623eae2785240b9bd12b16a66d81610", "metadata": { "execution": { - "iopub.execute_input": "2026-08-17T07:37:26.610032Z", - "iopub.status.busy": "2026-08-17T07:37:26.609896Z", - "iopub.status.idle": "2026-08-17T07:37:31.165724Z", - "shell.execute_reply": "2026-08-17T07:37:31.165278Z" + "iopub.execute_input": "2026-10-13T18:06:49.441611Z", + "iopub.status.busy": "2026-10-13T18:06:49.441529Z", + "iopub.status.idle": "2026-10-13T18:06:54.524179Z", + "shell.execute_reply": "2026-10-13T18:06:54.523552Z" } }, "outputs": [ @@ -373,13 +383,13 @@ "output_type": "stream", "text": [ "predict: exit 0\n", - "logs/predict/runs/2026-08-17_09-37-28/predictions/instance_table.csv\n", + "logs/predict/runs/2026-10-13_20-06-51/predictions/instance_table.csv\n", "instances: 1023\n", - "embedding dims: 128\n", + "embedding dims: 256\n", "composition : ['comp_Imm_other', 'comp_Tcell_CD4', 'comp_Endothelial', 'comp_Tcell_CD8', 'comp_Fibroblast', 'comp_Myeloid_CD11c', 'comp_Myeloid_CD68', 'comp_Epithelial']\n", "attention cols: ['attention', 'attention_c0', 'attention_logit_c0', 'attention_c1', 'attention_logit_c1']\n", - "spatial edges: 4554\n", - "cells: 17527 | cell-cell edges: 76310\n" + "spatial edges: 5700\n", + "cells: 17709 | cell-cell edges: 77456\n" ] } ], @@ -388,13 +398,16 @@ " [\n", " \"grass_mil.inference.predict\",\n", " f\"ckpt_path={ft_ckpt}\",\n", - " # Must match the checkpoint's architecture.\n", - " \"task=finetune_mil\",\n", - " \"model=supervised_module\",\n", + " # Must match the checkpoint's architecture and ego-graph depth.\n", + " \"regime=mil\",\n", " \"trainer=cpu\",\n", + " \"trainer.precision=32\",\n", " *DATA_ARGS,\n", - " *SAMPLER_ARGS,\n", + " *SPLIT_ARGS,\n", + " *EGO_ARGS,\n", " *LABEL_ARGS,\n", + " \"task.bag_key=patch_id\",\n", + " \"aggregation.bag_scope=patch\",\n", " \"interpretability.enabled=true\",\n", " \"interpretability.spatial.enabled=true\",\n", " \"interpretability.cells.enabled=true\",\n", @@ -461,10 +474,10 @@ "id": "938c804e27f84196a10c8828c723f798", "metadata": { "execution": { - "iopub.execute_input": "2026-08-17T07:37:31.166901Z", - "iopub.status.busy": "2026-08-17T07:37:31.166808Z", - "iopub.status.idle": "2026-08-17T07:37:46.980154Z", - "shell.execute_reply": "2026-08-17T07:37:46.979542Z" + "iopub.execute_input": "2026-10-13T18:06:54.526143Z", + "iopub.status.busy": "2026-10-13T18:06:54.526036Z", + "iopub.status.idle": "2026-10-13T18:07:04.844533Z", + "shell.execute_reply": "2026-10-13T18:07:04.843901Z" } }, "outputs": [ @@ -475,10 +488,9 @@ "interpretability report: exit 0\n", "report: report/interpretability_report/report.html\n", "niche_label\n", - "-1 111\n", - " 1 81\n", - " 2 377\n", - " 3 454\n" + "-1 257\n", + " 1 430\n", + " 2 336\n" ] } ], @@ -541,10 +553,10 @@ "id": "b43b363d81ae4b689946ece5c682cd59", "metadata": { "execution": { - "iopub.execute_input": "2026-08-17T07:37:46.981449Z", - "iopub.status.busy": "2026-08-17T07:37:46.981362Z", - "iopub.status.idle": "2026-08-17T07:37:46.992132Z", - "shell.execute_reply": "2026-08-17T07:37:46.991737Z" + "iopub.execute_input": "2026-10-13T18:07:04.845883Z", + "iopub.status.busy": "2026-10-13T18:07:04.845784Z", + "iopub.status.idle": "2026-10-13T18:07:04.853636Z", + "shell.execute_reply": "2026-10-13T18:07:04.853308Z" } }, "outputs": [ @@ -593,47 +605,36 @@ " \n", " \n", " -1\n", - " 0.160\n", - " 0.076\n", - " 0.021\n", - " 0.068\n", - " 0.059\n", - " 0.015\n", - " 0.025\n", - " 0.576\n", + " 0.144\n", + " 0.225\n", + " 0.035\n", + " 0.224\n", + " 0.137\n", + " 0.037\n", + " 0.041\n", + " 0.157\n", " \n", " \n", " 1\n", - " 0.233\n", - " 0.060\n", - " 0.000\n", - " 0.003\n", - " 0.084\n", - " 0.022\n", - " 0.044\n", - " 0.555\n", + " 0.167\n", + " 0.239\n", + " 0.025\n", + " 0.181\n", + " 0.218\n", + " 0.035\n", + " 0.035\n", + " 0.101\n", " \n", " \n", " 2\n", - " 0.196\n", - " 0.250\n", - " 0.034\n", - " 0.169\n", - " 0.291\n", - " 0.033\n", - " 0.022\n", - " 0.005\n", - " \n", - " \n", - " 3\n", - " 0.058\n", - " 0.254\n", - " 0.056\n", - " 0.330\n", - " 0.048\n", - " 0.068\n", " 0.043\n", - " 0.142\n", + " 0.190\n", + " 0.037\n", + " 0.266\n", + " 0.019\n", + " 0.047\n", + " 0.013\n", + " 0.384\n", " \n", " \n", "\n", @@ -642,17 +643,15 @@ "text/plain": [ " Imm_other Tcell_CD4 Endothelial Tcell_CD8 Fibroblast \\\n", "niche_label \n", - "-1 0.160 0.076 0.021 0.068 0.059 \n", - " 1 0.233 0.060 0.000 0.003 0.084 \n", - " 2 0.196 0.250 0.034 0.169 0.291 \n", - " 3 0.058 0.254 0.056 0.330 0.048 \n", + "-1 0.144 0.225 0.035 0.224 0.137 \n", + " 1 0.167 0.239 0.025 0.181 0.218 \n", + " 2 0.043 0.190 0.037 0.266 0.019 \n", "\n", " Myeloid_CD11c Myeloid_CD68 Epithelial \n", "niche_label \n", - "-1 0.015 0.025 0.576 \n", - " 1 0.022 0.044 0.555 \n", - " 2 0.033 0.022 0.005 \n", - " 3 0.068 0.043 0.142 " + "-1 0.037 0.041 0.157 \n", + " 1 0.035 0.035 0.101 \n", + " 2 0.047 0.013 0.384 " ] }, "execution_count": 7, @@ -675,16 +674,16 @@ "id": "8a65eabff63a45729fe45fb5ade58bdc", "metadata": { "execution": { - "iopub.execute_input": "2026-08-17T07:37:46.992960Z", - "iopub.status.busy": "2026-08-17T07:37:46.992876Z", - "iopub.status.idle": "2026-08-17T07:37:47.258570Z", - "shell.execute_reply": "2026-08-17T07:37:47.258190Z" + "iopub.execute_input": "2026-10-13T18:07:04.854694Z", + "iopub.status.busy": "2026-10-13T18:07:04.854612Z", + "iopub.status.idle": "2026-10-13T18:07:05.154766Z", + "shell.execute_reply": "2026-10-13T18:07:05.154272Z" } }, "outputs": [ { "data": { - "image/png": "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", + "image/png": "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UzsEoxIeoUP2YmpHYehT7En/mJikExEREZH6PuGe9I+BSToRERERqY496YaYpBMRERGR6jQi0EjyXeYptWdkTNKJiIiISHXsSTfEJJ2IiIiI1MeadANM0omIiIhIdexJN8QknYiIiIhU96ndcfRjY5JOREREROqTVHrMmaQTEREREalAJGFKaZ6JYZJORERERKpjTbohJulEREREpDom6YaYpBMRERGR6pikGzJTOwAiIiIiIqUmPaXpLS1atAguLi6wtrZGuXLlcOTIkRSXffToETp06IDixYvDzMwMQ4cOfY8N+TCYpBMRERGR6vQ96SlNb2PDhg0YOnQoxo4di3PnzsHb2xsNGzZEUFBQssvHxMTAwcEBY8eOhYeHxwfYmvfHJJ2IiIiIVKcfJz2l6W3MmjULX375JXr27ImSJUtizpw5cHJywuLFi5Nd3tnZGXPnzkWXLl2QNWvWD7A1749JOhERERGpTqOTVCcACA8PN5hiYmKSrCc2NhZnz55FvXr1DNrr1auH48ePp8u2fAhM0omIiIhIffKGCYCTkxOyZs2qTFOmTEmymmfPnkGr1cLR0dGg3dHREcHBwR91Ez4kju5CRERERKrTaAUas+TrWjTahPZ79+7B3t5eabeyskp5fRqNwWMRSdJmzJikExEREZH6EvWYJzsPgL29vUGSnpxcuXLB3Nw8Sa/5kydPkvSuGzOWuxARERGR6jSSSk36WwzBaGlpiXLlysHPz8+g3c/PD1WqVPnQYX807EknIiIiItWlNorL247uMnz4cHTu3Bnly5dH5cqVsWTJEgQFBaFv374AgDFjxuDBgwdYvXq18pzz588DACIjI/H06VOcP38elpaWKFWq1Ltszntjkk5EREREqks8ikty895G27ZtERISgkmTJuHRo0coXbo09uzZg0KFCgFIuHnR62OmlylTRvn/2bNn8dtvv6FQoUK4c+fO223IB8IknYiIiIjUp/tvSmneW+rfvz/69++f7LxVq1YlaZN3uKvpx8QknYiIiIhU9yF70jOCT+rC0c8//xy3bt1KdZlu3bphwYIF7/U6Dx8+RP369VG8eHG4u7ujTZs2CA0Nfa91EhEREVEqRFKfTMwnlaTv2bMHRYoU+eivY25ujnHjxuH69eu4ePEiChUqhNGjR3/01yUiIiIyVWm546gpMbokXaPRYOrUqahY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", "text/plain": [ "
" ] @@ -724,16 +723,16 @@ "id": "4dd4641cc4064e0191573fe9c69df29b", "metadata": { "execution": { - "iopub.execute_input": "2026-08-17T07:37:47.259844Z", - "iopub.status.busy": "2026-08-17T07:37:47.259692Z", - "iopub.status.idle": "2026-08-17T07:37:47.383135Z", - "shell.execute_reply": "2026-08-17T07:37:47.382656Z" + "iopub.execute_input": "2026-10-13T18:07:05.156337Z", + "iopub.status.busy": "2026-10-13T18:07:05.156157Z", + "iopub.status.idle": "2026-10-13T18:07:05.275706Z", + "shell.execute_reply": "2026-10-13T18:07:05.275159Z" } }, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkMAAAJOCAYAAAC0pwNFAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvctoD+AAAAAlwSFlzAAAPYQAAD2EBqD+naQAAz6NJREFUeJzs3Xl8E3X6B/DPJGnTi96UQkuhXK2lrSAgclQQwa4IooC6KqfoCux6rOsByqp44rmILqDLWql4ofLjVLsgChQPDoEWSytHS2mgLT3TtE3aJPP7Y5qQNEmbY5KZJM/79eJVMplMJj0yT57v832+DMuyLAghhBBC/JRE6BMghBBCCBESBUOEEEII8WsUDBFCCCHEr1EwRAghhBC/RsEQIYQQQvwaBUOEEEII8WsUDBFCCCHEr1EwRAghhBC/RsEQIYQQQvwaBUOEdCguLgbDMNi0aZNDj/vll1/AMAy2bt3qnhNzgLOvwVuf9+zZs5DL5Thw4IBxmzt+Ho68PpVKBYZh8NJLL/H2/O6WmpqK2bNnC30avNHr9UhPT8eKFSuEPhXiJSgYIoS4xfHjx8EwDD7//HO3PccTTzyBCRMmICsry23P4Y369++PP//5z0KfhmAkEglWrlyJt956C+Xl5UKfDvECMqFPgBCxSE1Nhbcv1SfUaxDieYuLi/F///d/2L59u0ef1xcVFxcLfQq8u/322xEdHY1//etf+Ne//iX06RCRo8wQIcQrrV+/HrGxsbj55puFPhUiQhKJBHfffTc2btwItVot9OkQkaNgiPg00/qRnJwcDBo0CEFBQRg5ciQOHjxotq+tupDW1lY899xzuOqqqxAUFIR+/frhb3/7G2pqaiyer7vnMBzvn//8J4YMGQK5XI64uDgsWLAAlZWVxn30ej1eeeUVXHXVVQgNDUXfvn3x5z//udtP8NZegyPfA2vsOZfOz7t161YMHz4cAHD33XeDYRgwDIPnn38eAPDVV1+BYRgcOXLE7LkaGhrAMAxWrVrV7Xnt2LEDWVlZkMlsJ7j5+nnYcv78ecycORM9evRATEwMlixZgtbW1m4fZ0qv1+Ott95CWloa5HI5YmJiMHv2bLPvr1arBcMwVmtgJk6ciOuuu854m2EYnD9/Hl988YXx+z5s2LAuz8FazVB8fDwWLFiAQ4cOYcyYMQgJCcGwYcOQn58PAPj5558xduxYhISEYPDgwdi2bZvVczOcg1QqRXx8PO655x6UlZWZ7dfW1oannnoKffr0QWhoKCZNmoTi4mJMnjwZI0eOtDju5s2bMXbsWISGhiI0NBQTJ040qxszmDRpEurr663eR4gpCoaIX/j8889x7tw55Ofn448//kBYWBhmzJgBlUrV5eM0Gg0mTZqE999/HytXrsTFixeRn5+PtLQ0bNy40eHnaGtrw5QpU5CTk4O33noLly9fxo8//oizZ8/i+uuvR1NTEwDg1VdfxSuvvII333wT1dXVOHLkCGbPno23337b498DZ87ltttuw7FjxwAAn332GViWBcuyxmDIVZcuXcK5c+esXigN+Px5WNPQ0IAJEybgjz/+wN69e3Hu3DlkZWXh73//u0OvZfHixXj66afxyCOPoLKyEvv370dlZSWuu+46nD592qFjAQDLsujXrx/uuusu4/f9+PHjDh8HACoqKvDGG28gNzcX5eXlSEtLw7Rp0/Dzzz9j1apVyMnJwYULFzBmzBjcdddduHjxotnjf/zxR+M5tLS0YOfOnTh//jymTZtmlq257777sHbtWrz11lu4ePEi3nzzTfzjH/+AUqm0OKeXX34Z99xzD2699VacOXMG586dw+jRo3HjjTdaBD3XXnstAGD//v1OvX7iR1hCfNjPP//MAmCnTp1qtv3o0aMsAHbjxo3GbadOnWIBsB9//LFx2+rVq1kAbF5eHi/P8e9//5sFwP7www9m+yoUClYul7NvvPEGy7IsO3nyZDYrK8vh12vtNThyftbYcy7WnvfYsWMsAPazzz6z2P/LL79kAbCHDx82215fX88CYF999dUun++XX35hAbAffvihxX3u+HlYe30vv/wyC4AtKCgwe+xLL73EAmBffPHFLl8Dy7Ls77//zgJgn3zySbPtVVVVbEhICHvXXXexLMuy7e3tLAD2mWeesTjGhAkT2NGjR5tt69evn/Gx9khJSWFnzZpltq1Xr15sjx492Lq6OuM2hULBAmB79erF1tTUGLdXVlayDMOwr732WrfPdfz4cRYAu2fPHpZlWbawsJAFwL788stm+506dYplGIYdMWKEcVtZWRkrk8nYRx55xOK4WVlZ7Lhx4yy2BwQEsHPnzu32vIh/o8wQ8Qu33HKL2e309HQAwLlz57p83DfffIPY2FjcdNNNvDzHjh07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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkYAAAJOCAYAAABSjsgBAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAADt/ElEQVR4nOy9eXxU1f3//7qzJ5nsC0lMAiEgCElAoVBUNkVkE1GqtioFl7pLcflUbX9WqR+XautCreKnbrji1wXrSkURKoIiKGtEMQZCIIFAQpJJMus9vz8mM8xM7p2569w7M+f5ePDQmdy599ztnNd5b4chhBBQKBQKhUKhUGDQugEUCoVCoVAoeoEKIwqFQqFQKJQ+qDCiUCgUCoVC6YMKIwqFQqFQKJQ+qDCiUCgUCoVC6YMKIwqFQqFQKJQ+qDCiUCgUCoVC6YMKIwqFQqFQKJQ+qDCiUCgUCoVC6YMKIwolBoMGDcKiRYtE/Wbfvn1gGAZ/+9vf1GmUCNatWweGYbBu3bq4HpdhGNx7773Bz3V1dbj33nuxb98+VY53/PhxFBQUYOXKlcHvFi1aBLvdruhxpkyZgilTpsTcLvAMvPjii4oeXygfffRR2PWPxaJFizBo0CDV2hNvFixYgHnz5mndDEoCQoURhRKDVatW4e6779a6GZI57bTTsGnTJpx22mlxPe6mTZtw9dVXBz/X1dVh6dKlqgmjpUuXorS0FJdccokq+080PvroIyxdulTw9nfffTdWrVqlYoviy7333osPP/wQa9eu1boplATDpHUDKBS9c+qpp2rdBFlkZWXhl7/8ZdyPG89jtrW14ZlnnsFjjz0GhmHidtxkoqqqSusmKEpVVRVmzJiBhx56CGeddZbWzaEkENRiRElJ7r33XjAMg927d+M3v/kNsrOzMWDAAFx55ZXo6OgI25bLlXb8+HHcdtttGDx4MKxWK4qKijBr1izs2bOn37EeffRRVFZWwm63Y8KECfjqq6/6bbNlyxbMnTsXeXl5sNlsOPXUU/H//t//C9ump6cHt99+OyorK2Gz2ZCXl4exY8fi9ddfj3quXK60gIvpp59+wqxZs2C321FeXo7bbrsNLpcrxtUD1q5diylTpiA/Px9paWmoqKjA/Pnz0dPTE9wm1JX24osv4qKLLgIATJ06FQzDhLmZ+NyVQt1WL774IrxeL6+1SMh5ut1u/O///i+GDx8Oq9WKwsJCXHHFFWhtbY15/EOHDuHiiy9GZmYmsrOzcckll6ClpSXm70JpbGzE5ZdfjqKiIlitVpxyyin4+9//DpZlg9vwuUUj3XaLFi3CP//5TwAIXmuGYaJa67hcaQzD4KabbsILL7yAYcOGIS0tDWPHjsVXX30FQggeeeSR4LN91lln4aeffgr7/Zo1a3D++eejrKwMNpsNQ4YMwbXXXoujR4/2O/6///1v1NbWwmq1YvDgwXjiiSeC72kohBA89dRTGD16NNLS0pCbm4tf/epX+Pnnn/vtc8GCBfj0009RX1/Pe94USiTUYkRJaebPn49LLrkEV111FXbu3Im77roLAPD888/z/qarqwtnnnkm9u3bhzvuuAPjx4+Hw+HAf//7XzQ3N2P48OHBbf/5z39i+PDhePzxxwH43RWzZs1CQ0MDsrOzAQCff/45ZsyYgfHjx2P58uXIzs7GypUrcckll6CnpycoGG699Va8/PLL+N///V+ceuqp6O7uxq5du3Ds2DFJ5+7xeDB37lxcddVVuO222/Df//4X9913H7Kzs/HnP/+Z93f79u3D7NmzMXHiRDz//PPIycnBwYMHsXr1arjdbqSnp/f7zezZs/HAAw/gj3/8I/75z38G3XpKWSk+/PBDnHrqqcjJyZF0nizL4vzzz8cXX3yBP/zhDzj99NOxf/9+3HPPPZgyZQq2bNmCtLQ0zmP39vZi2rRpOHToEB588EGcfPLJ+PDDD0W59FpbW3H66afD7Xbjvvvuw6BBg/DBBx/g9ttvR319PZ566ilR1+Puu+9Gd3c33nrrLWzatCn4fUlJiaj9AMAHH3yA7777Dg899BAYhsEdd9yB2bNnY+HChfj555/x5JNPoqOjA7feeivmz5+Pbdu2BcVMfX09JkyYgKuvvhrZ2dnYt28fHn30UZx55pnYuXMnzGYzAGD16tW48MILMWnSJLzxxhvwer3429/+hsOHD/drz7XXXosXX3wRixcvxl//+le0tbXhL3/5C04//XRs374dAwYMCG47ZcoUEELw0Ucf4eabbxZ97pQUhVAoKcg999xDAJCHH3447PsbbriB2Gw2wrJs8LuBAweShQsXBj//5S9/IQDImjVrePff0NBAAJCamhri9XqD32/evJkAIK+//nrwu+HDh5NTTz2VeDyesH3MmTOH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/ND27duxaNEiRd/505/+FF/4whewd+9eXH755fja176GW265BV/96lfx4YcfYvbs2bjlllsQfe52bW0tZs6cie3btyv6ToIg9AcJI4IgdMmXv/xl3HHHHbj55ptx5513wul04rHHHhO9fnBwEIODg6isrFT8fevWrcOcOXPwgx/8AMPDw/j85z+P6667DnPnzsV3v/tdfPTRRzh58mTM+8466yx8+umnir6TIAj9QcKIIAjd8pOf/ATBYBD/+Z//id/97ndwOp2i146NjQFA0muSMX/+/Mj/y8rKAACNjY1Tnjt16lTM+/Ly8jA6OqroOwmC0B8kjAiC0C1Hjx7FiRMnIAgCjh07lvTaGTNmgOM4DAwMxDzP83yM+wsAxsfHp7zfZrNF/s9xnOhz8dvzz5w5g9LSUgm1IQjCCJAwIghClwQCAdx888244YYb8Mgjj+D222+f4saKxm63o6GhAe3t7THPl5aWoqenJ/I4FArh4MGDqpTR5/Oho6MDCxYsUOXzCILIPiSMCILQJQ899BCGhobw5JNP4oEHHsC5556L22+/Pel7vvjFL2LHjh0xz1188cV488038eabb+Lvf/877rrrLgwODqpSxl27dsHhcKC5uVmVzyMIIvuQMCIIQnds27YNP/vZz/DSSy/B7XaD53m89NJL2LFjB5555hnR991xxx146623MDQ0FHnutttuw9q1a3HLLbdgxYoVqKurw0UXXaRKOf/jP/4DN998M/Lz81X5PIIgsg/H4p3vBEEQBub666/HggUL8OCDD2r6PadPn8Y555yD3bt3o66uTtPvIggic5DFiCAIU/Hv//7vcLlcmn9PZ2cnnn76aRJFBGEyyGJEEARBEAQxCVmMCIIgCIIgJiFhRBAEQRAEMQkJI4IgCIIgiElIGBEEQRAEQUxCwoggCIIgCGISEkYEQRAEQRCTkDAiCIIgCIKYhIQRQRAEQRDEJCSMCIIgCIIgJiFhRBAEQRAEMcn/B8c5mMWeYsomAAAAAElFTkSuQmCC", "text/plain": [ "
" ] @@ -785,18 +784,26 @@ "id": "cb1e1581032b452c9409d6c6813c49d1", "metadata": { "execution": { - "iopub.execute_input": "2026-08-17T07:37:47.384629Z", - "iopub.status.busy": "2026-08-17T07:37:47.384536Z", - "iopub.status.idle": "2026-08-17T07:37:47.979578Z", - "shell.execute_reply": "2026-08-17T07:37:47.978786Z" + "iopub.execute_input": "2026-10-13T18:07:05.277201Z", + "iopub.status.busy": "2026-10-13T18:07:05.277118Z", + "iopub.status.idle": "2026-10-13T18:07:05.860430Z", + "shell.execute_reply": "2026-10-13T18:07:05.859823Z" } }, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/local/home/lrabuzin/anaconda3/envs/spatial_leomed/lib/python3.11/site-packages/torchvision/io/image.py:13: UserWarning: Failed to load image Python extension: '/local/home/lrabuzin/anaconda3/envs/spatial_leomed/lib/python3.11/site-packages/torchvision/image.so: undefined symbol: _ZN3c1017RegisterOperatorsD1Ev'If you don't plan on using image functionality from `torchvision.io`, you can ignore this warning. Otherwise, there might be something wrong with your environment. Did you have `libjpeg` or `libpng` installed before building `torchvision` from source?\n", + " warn(\n" + ] + }, { "name": "stdout", "output_type": "stream", "text": [ - "cells: 17527 | all labelled: True\n" + "cells: 17709 | all labelled: True\n" ] } ], @@ -836,10 +843,10 @@ "id": "277c27b1587741f2af2001be3712ef0d", "metadata": { "execution": { - "iopub.execute_input": "2026-08-17T07:37:47.980867Z", - "iopub.status.busy": "2026-08-17T07:37:47.980771Z", - "iopub.status.idle": "2026-08-17T07:37:49.313307Z", - "shell.execute_reply": "2026-08-17T07:37:49.312756Z" + "iopub.execute_input": "2026-10-13T18:07:05.862078Z", + "iopub.status.busy": "2026-10-13T18:07:05.861991Z", + "iopub.status.idle": "2026-10-13T18:07:06.707237Z", + "shell.execute_reply": "2026-10-13T18:07:06.706801Z" } }, "outputs": [ @@ -847,12 +854,12 @@ "name": "stdout", "output_type": "stream", "text": [ - "niche 3: 8108 cells, 35502 edges\n" + "niche 1: 7214 cells, 31380 edges\n" ] }, { "data": { - "image/png": "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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAn4AAAIWCAYAAADJd4BXAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAACKyUlEQVR4nOzdeXhM59sH8O8kJBKSWEKWCokSIRKRRNS+1FpLVe1q31IURRddNLSkSlXtVC2ttaid2sUWu9QepXZCEZkQss39/pFfzmuESDgxZybfz3Wdq50zZ55zn5nI3Lmf5ehEREBEREREFs/K1AEQERER0evBxI+IiIgol2DiR0RERJRLMPEjIiIiyiWY+BERERHlEkz8iIiIiHIJJn5EREREuQQTPyIiIqJcgokfERERUS7BxI8sgk6nQ1hYmPJ4586d0Ol02Llz5wtfu2/fPoSFheH+/fs5Fp85S0hIQFhYWJbeS3M4z8sKCwuDTqfL9uvq1KmDChUq5EBEOe/GjRsICwtDVFTUaznfvHnzoNPpcOnSpWy9rlu3bihQoEDOBPWEa9euYfDgwahduzYKFiwInU6HefPm5fh5idTExI9yvX379mHkyJFM/J4jISEBI0eOfC2J3+s4z8vq1asXIiMjTR3Ga3Xjxg2MHDnytSV+TZs2RWRkJNzc3F7L+bLr/PnzWLhwIWxsbPDOO++YOhyil5LH1AEQEWlZQkIC7O3tUbx4cRQvXtzU4Vi0okWLomjRoqYO47lq1aqF//77DwBw+PBhLF682MQREWUfK35kMmfPnkWHDh3g4uICW1tblChRAl26dEFiYqJyTExMDPr27YvixYvDxsYGXl5eGDlyJFJSUlSJISwsDJ988gkAwMvLCzqdTuki7tmzJwoXLoyEhIQMr6tXrx58fX2VxzqdDgMGDMDMmTPh7e0NW1tblC9fHkuWLMnw2le9pkWLFqFq1aooUKAAChQogICAAPz6669Gx8yZMwcVK1ZEvnz5ULhwYbz33ns4c+aM0THp3WPnz5/HO++8gwIFCsDDwwNDhw5VPoNLly4pX8QjR45U3p9u3boBSKuAdO/eHWXKlIG9vT3eeOMNNG/eHCdOnMgQ9/379zF06FCUKlUKtra2KFasGN555x2cPXv2hed5Hr1ej2HDhsHLyws2NjZ44403MHjwYDx8+NDouPTP5/fff0e5cuVgb2+PihUrYt26dUbHpXfnHj16FK1bt0ahQoXw5ptvGj33Mp8HABw6dAg1a9aEvb09SpUqhe+//x4Gg0F5Pn14wqJFi/DZZ5/Bzc0NBQoUQPPmzXHr1i3Ex8ejT58+cHZ2hrOzM7p3744HDx4YnUNEMG3aNAQEBMDOzg6FChVC69at8e+//xodl979nFlMO3fuROXKlQEA3bt3Vz6TJ4dUZFVW3//ndfX+9ddfePvtt+Hk5AR7e3uUK1cO4eHhGc6T2c9yuqSkJHz33Xfw8fGBra0tihYtiu7duysJXWasrPiVSRZAiEwgKipKChQoIJ6enjJjxgzZtm2bLFiwQNq2bSt6vV5ERG7evCkeHh5SsmRJmTlzpmzdulW+/fZbsbW1lW7duhm1B0C++eYb5fGOHTsEgOzYsSPTOK5evSofffSRAJA///xTIiMjJTIyUuLi4uTvv/8WAPLLL78YvebUqVMCQKZOnWp0fg8PDylfvrwsXrxY1qxZI40bNxYAsmzZMuW47FzTs3z99dcCQFq1aiXLli2TzZs3y4QJE+Trr79WjhkzZowAkA4dOsj69evlt99+k1KlSomTk5OcO3dOOa5r165iY2Mj5cqVk/Hjx8vWrVtlxIgRotPpZOTIkSIi8vjxY/nrr78EgPTs2VN5f86fPy8iIhERETJ06FBZvny5REREyMqVK6Vly5ZiZ2cnZ8+eVc6l1+vF19dX8ufPL6NGjZJNmzbJihUrZNCgQbJ9+/YXnudZHj58KAEBAeLs7CwTJkyQrVu3ys8//yxOTk5Sr149MRgMRp+Pp6enhISEyB9//CEbNmyQOnXqSJ48eeTChQvKcd98840AkJIlS8pnn30mW7ZskVWrVhk9l93Po3bt2lKkSBEpU6aMzJgxQ7Zs2SL9+vUTADJ//nzluPSf2ZIlS0q3bt3kr7/+khkzZkiBAgWkbt260qBBAxk2bJhs3rxZxo4dK9bW1vLRRx8ZxdO7d2/JmzevDB06VP766y9ZtGiR+Pj4iIuLi8TExGQrpri4OJk7d64AkK+++kr5TK5evfrcz+R5svr+p5/v4sWLyr7Zs2eLTqeTOnXqyKJFi2Tr1q0ybdo06devn3JMVn6WRURSU1OlcePGkj9/fhk5cqRs2bJFZs+eLW+88YaUL19eEhISsnxNhw4dEgAyd+7cbL8fRKbExI9Mol69elKwYEG5ffv2c4/p27evFChQQC5fvmy0f/z48QJATp06pex72cRPRGTcuHEZvmzS1a5dWwICAoz2ffjhh+Lo6Cjx8fFG57ezszP6ck1JSREfHx8pXbr0S13T0/7991+xtraWTp06PfeY2NhYsbOzk3feecdo/5UrV8TW1lY6duyo7OvatasAkD/++MPo2HfeeUfKli2rPP7vv/8yvL/Pk5KSIklJSVKmTBn5+OOPlf2jRo0SALJly5bnvjY75xERCQ8PFysrKzl06JDR/uXLlwsA2bBhg7IPgLi4uCh/VIiIxMTEiJWVlYSHhyv70pO7ESNGZDjf04lfVj4PkbSfIQBy4MABo/3ly5eXRo0aKY/Tf2abN29udNzgwYMFgAwcONBof8uWLaVw4cLK48jISAEgP/74o9FxV69eFTs7O/n000+zHZNayU1W3/+nE7/4+HhxdHSUGjVqGCXyT8vqz/LixYsFgKxYscLouPTrnDZtWpaviYkfmSvWrem1S0hIQEREBNq2bZvpeJ5169ahbt26cHd3R0pKirI1adIEABAREZHlc4qIURtZ7VYdNGgQoqKisHfvXgBpXYu///47unbtmmEW4dtvvw0XFxflsbW1Ndq1a4fz58/j2rVrr3xNW7ZsQWpqKvr37//cYyIjI/Ho0aMMXaQeHh6oV68etm3bZrRfp9OhefPmRvv8/f1x+fLl557jSSkpKRgzZgzKly8PGxsb5MmTBzY2Nvjnn3+MupY3btwIb29v1K9fP0vtZsW6detQoUIFBAQEGL2XjRo1euaM7rp168LBwUF57OLigmLFij3zWt9///0Xnj8rn0c6V1dXhISEGO173vvcrFkzo8flypUDkDbx4en99+7dU7p7161bB51Ohw8++MDo/XB1dUXFihUzvB/ZiUkN2Xn/0+3btw96vR79+vV74YzqrPwsr1u3DgULFkTz5s2N3qOAgAC4urpqdmJRbvH48WPo9XrVt8ePH5v60jSFkzvotYuNjUVqauoLB8rfunULa9euRd68eZ/5/J07d7J8zoiICNStW9do38WLF+Hp6Znp69599114enpi6tSpqF69OubNm4eHDx8+88ve1dX1ufvu3r2L4sWLv9I1pY9Byux9u3v3LgA8c1aku7s7tmzZYrTP3t4e+fLlM9pna2ub5V+UQ4YMwdSpU/HZZ5+hdu3aKFSoEKysrNCrVy88evTIKPYSJUpkqc2sunXrFs6fP5/l97JIkSIZjrG1tTWKM11WZpVm5fN4mXMXLlzY6LGNjU2m+x8/fowCBQrg1q1bEBGjPz6eVKpUqZeOSQ0vc77svMdZ+Vm+desW7t+/r7x3T8vO7xRS1+PHj1HErgASkKp6266urrh48WKGn4/ciokfvXaFCxeGtbW1UgV7HmdnZ/j7+2P06NHPfN7d3T3L5wwKCsKhQ4ey/XorKyv0798fX3zxBX788UdMmzYNb7/9NsqWLZvh2JiYmOfuS//Se5VrSq+OXrt2DR4eHs88Jv08N2/ezPDcjRs34Ozs/Nz2X8aCBQvQpUsXjBkzxmj/nTt3ULBgQeVx0aJFX/h5Z5ezszPs7OwwZ86c5z7/srKyXl9WPo/XydnZGTqdDrt374atrW2G55+1T+uefI/V4OzsjCJFiuCvv/565vNPViTp9UpKSkICUtEFb8BGxXmnSTDgt5jrSEpKYuL3P0z86LWzs7ND7dq1sWzZMowePfq5X9DNmjXDhg0b8Oabb6JQoUKvdE4HBwcEBwc/87n0L8TnVR569eqFsLAwdOrUCdHR0Rg7duwzj9u2bRtu3bqlVFxSU1OxdOlSvPnmm0rF4lWuqWHDhrC2tsb06dNRtWrVZx5TtWpV2NnZYcGCBWjTpo2y/9q1a9i+fTtat26drXMCmb8/Op0uQ0Kxfv16XL9+HaVLl1b2NWnSBCNGjMD27dtRr169bJ/nWZo1a4YxY8agSJEi8PLyytJr1JSVz+N1atasGb7//ntcv34dbdu2VaXN7H4maqtWrRqcnJwwY8YMtG/f/qUW0H5Ss2bNsGTJEqSmpqJKlSoqRUlqsoEVbHQqjkIT9ZqyFEz8yCQmTJiAGjVqoEqVKvj8889RunRp3Lp1C2vWrMHMmTPh4OCAUaNGYcuWLahWrRoGDhyIsmXL4vHjx7h06RI2bNiAGTNmqLKump+fHwDg559/RteuXZE3b16ULVtW+eu/YMGC6NKlC6ZPn46SJUtmGEeUztnZGfXq1cPXX3+N/PnzY9q0aTh79qzRki6vck2enp744osv8O233+LRo0fo0KEDnJyccPr0ady5cwcjR45EwYIF8fXXX+OLL75Aly5d0KFDB9y9excjR45Evnz58M0332T7/XFwcEDJkiWxevVqvP322yhcuDCcnZ3h6emJZs2aYd68efDx8YG/vz+OHDmCcePGZbiGwYMHY+nSpXj33Xfx+eefIyQkBI8ePUJERASaNWumjP963nmeZfDgwVixYgVq1aqFjz/+GP7+/jAYDLhy5Qo2b96MoUOH5uiXe1Y+j9epevXq6NOnD7p3747Dhw+jVq1ayJ8/P27evIk9e/bAz88PH374YbbafPPNN2FnZ4eFCxeiXLlyKFCgANzd3eHu7o5Lly7By8sLXbt2zbG7VxQoUAA//vgjevXqhfr166N3795wcXHB+fPn8ffff2PKlCnZaq99+/ZYuHAh3nnnHQwaNAghISHImzcvrl27hh07duDdd9/Fe++9l2kby5cvBwBliZzDhw8r431f5g8rMmat08H6FRN8o/agY/L3FCZ+ZBIVK1bEwYMH8c0332D48OGIj4+Hq6sr6tWrp4y/cXNzw+HDh/Htt99i3LhxuHbtGhwcHODl5YXGjRu/chUwXZ06dTB8+HDMnz8fv/zyCwwGA3bs2IE6deoox7Rr1w7Tp0/Hhx9++Ny1vFq0aAFfX1989dVXuHLlCt58800sXLgQ7dq1U4551WsaNWoUypQpg8mTJ6NTp07IkycPypQpg4EDByrHDB8+HMWKFcOkSZOwdOlS2NnZoU6dOhgzZgzKlCnzUu/Rr7/+ik8++QQtWrRAYmKi8mX/888/I2/evAgPD8eDBw8QGBiIP//8E1999ZXR6x0cHLBnzx6EhYVh1qxZGDlyJAoVKoTKlSujT58+LzzPs+TPnx+7d+/G999/j1mzZuHixYuws7NDiRIlUL9+/ReO31RDVj6P12nmzJl46623MHPmTEybNg0GgwHu7u6oXr16hokcWWFvb485c+Zg5MiRaNiwIZKTk/HNN98gLCxMmVSS03fZ6NmzJ9zd3TF27Fj06tULIgJPT0907do1221ZW1tjzZo1+Pnnn/H7778jPDwcefLkQfHixVG7dm3lj8DMPFlJB4CpU6di6tSpANImkRFpnU74k0r0QkOHDsX06dNx9erVZw5S1+l06N+/f7YrEETmatq0afj0009x4cKF504oIcoqvV4PJycnfGhVArYqdvUmigHTDVcQFxcHR0dH1do1Z6z4EWVi//79OHfuHKZNm4a+ffs+M+kjyo127NiBgQMHMukjMjNM/IgyUbVqVdjb26NZs2b47rvvTB0OkWYsW7bM1CGQBcqRMX5khIkfUSayOhKCIyaIiF6dtS5tU6099ZqyGLxzBxEREVEuwYofERERaQK7enMeK35EREREuQQrfhpnMBhw48YNODg4vPKq9URERNkhIoiPj4e7u/tz1zBVkynH+KWkpCAsLAwLFy5ETEwM3Nzc0K1bN3z11VfKtYsIRo4ciVmzZiE2NhZVqlTB1KlT4evrq17QOYyJn8bduHFDE/cBJSKi3Ovq1auq3ClJy8aOHYsZM2Zg/vz58PX1xeHDh9G9e3c4OTlh0KBBAIAffvgBEyZMwLx58+Dt7Y3vvvsODRo0QHR0tNnc65mJn8al/yBd3L8FjgXymzgadegLvv77qlLW2aj557YGGCxwwrWNzmDqEFQlat6bVQN0YjmfT3x8PEp7l31tSY0px/hFRkbi3XffRdOmTQGk3ZZx8eLFOHz4MIC0at/EiRPx5ZdfolWrVgCA+fPnw8XFBYsWLULfvn1VizsnMfHTuPTuXccC+eHoUMDE0aiEq6drGhM/7WPip22WlPile11DjXRQd/JBetR6vd5ov62tLWxtbY321ahRAzNmzMC5c+fg7e2Nv//+G3v27MHEiRMBABcvXkRMTAwaNmxo1E7t2rWxb98+Jn5EREREWvD0kKn0e04/6bPPPkNcXBx8fHxgbW2N1NRUjB49Gh06dAAAxMTEAECGu9W4uLjg8uXLORe8ypj4ERERkSbkVFfv1atXje7V+3S1DwCWLl2KBQsWYNGiRfD19UVUVBQGDx4Md3d3dO3aVTnu6eqniJjV5EsmfkRERGTRHB0djRK/Z/nkk0/w+eefo3379gAAPz8/XL58GeHh4ejatStcXV0BQJnxm+727dtmdc9qyxpYQURERGYrfTkXNbesSkhIyLBkjbW1NQyGtDGbXl5ecHV1xZYtW5Tnk5KSEBERgWrVqqly/a8DK35ERESkCWnJmppdvVnXvHlzjB49GiVKlICvry+OHTuGCRMmoEePHgDSungHDx6MMWPGoEyZMihTpgzGjBkDe3t7dOzYUbWYcxoTPyIiIsr1Jk+ejK+//hr9+vXD7du34e7ujr59+2LEiBHKMZ9++ikePXqEfv36KQs4b9682WzW8AMAnYhY4GIHlkOv18PJyQl3T+6zmOVc9IXeNHUIlAku56J9XM5F2yxpORe9Xg8XN3fExcW9cIzcq57HyckJo/OXQj5ddup0mXssqfjy4b85Hr85sax/bURERET0XOzqJSIiIk0w5Z07cgtW/IiIiIhyCVb8iIiISBOssrkEywvbU68pi8HEj4iIiDSBXb05j8kwERERUS7Bih8RERFpQnbvtvHC9tRrymKw4kdERESUS7DiR0RERJrAil/OY8WPiIiIKJdgxY+IiIg0gbN6cx4TPyIiItIEa6jc1WuB9+p+VZrp6vX09ISPjw8CAgKU7fTp09lu4+TJk5kec//+ffzwww9G++rUqYN169ZlO+YnXzdixAgsXbo0W68hIiIiep00VfFbvnw5KlSokKPnSE/8Pv30U1XbHTVqlKrtERER5TZWKnf1WqnYlqXQTMXveXQ6HcaOHYsqVarAy8sLc+fOVZ7bvXs3/Pz8EBISggEDBkDk/2u6hw8fRtWqVeHv74+QkBDs3bsXABAaGor79+8jICAAwcHBRm3VrFkTb775JkJDQ5X98fHx6N27N0JCQuDv74/Q0FAkJydniLNbt26YMmUKAGDbtm2oWrUqKlWqhAoVKhjFTERERGQqmkr8WrdubdTVm5SUBADIly8fDhw4gA0bNmDgwIFISUlBYmIi2rdvj8mTJ+PgwYOoVasWrly5AgBISkpCq1atEBYWhuPHj2PChAlo3bo1Hj58iBkzZqBgwYKIiorC4cOHlXNfuHABO3fuxMmTJ7Fp0yZERkYCAIYOHYpatWrh4MGD+Pvvv5GSkqIkeM8TGBiIPXv24NixY9i1axdGjhyJmzdvZuk9SExMhF6vN9qIiIhyg/TlXNTcyJhZdPV26tQJAFCuXDnkyZMHMTExuHfvHuzt7VGnTh0AQNu2bdGnTx8AQHR0NGxsbNCoUSMAQI0aNVCsWDEcP34cbm5uzzx3+/btYW1tDTs7OwQEBODChQuoWrUqVq1ahf379+PHH38EADx69Ag2NjaZXsfdu3fRs2dPnDt3Dnny5MGdO3dw6tSp5577SeHh4Rg5cuQLjyMiIiLKLk0lfs+TL18+5f+tra2RkpJi1K37NBGB7hn9+s/al9k50ttatWoVSpUqleV4Q0ND0bx5c6xYsQI6nQ6BgYF4/Phxll47fPhwDBkyRHms1+vh4eGR5XMTERGZK9WXc+EYvww01dWbHT4+Pnj06BF27doFIK1aGBcXpzyXmJiI7du3AwD27duH27dvw8/PD46OjkhISFASuxdp0aIFvv/+e+X42NhYnD9/PtPXxMbGomTJktDpdNi1axf+/vvvLF+Xra0tHB0djTYiIqLcgF29OU9TFb/WrVsbVd4mT5783GNtbW2xePFi9OvXD3Z2dqhTpw5KlCgBALCxscGKFSswcOBAPHz4EPny5cOyZcuQP39+5M+fH506dYKfnx/y589vNM7vWSZOnIjPPvsMAQEBsLKyQt68eTF27FiULl36ua/5/vvv0a9fP3z//fcoX748qlSpks13goiIiEh9Osmsz5RMTq/Xw8nJCXdP7oOjQwFTh6MKfaE3TR0CZcLGwv5ENljgbzgbncHUIahKdGbb+fRMOrGcz0ev18PFzR1xcXE52gOV/l23uJgP7K3Uu8NugiEVHW6fzfH4zYll/WsjIiIioufSVFcvERER5V5WOp2qiy5zAeeMmPgRERGRJuisddBZqZesZbaaR27Frl4iIiKiXIIVPyIiItIEK2sdrFSs+LGrNyNW/IiIiIhyCVb8iIiISBusraCzUrEmpbPA9ZxeESt+RERERLkEK35ERESkCTorHXQqLiKvA8f4PY2JHxEREWmClbUOViomflZM/DJgVy8RERFRLsGKHxEREWmCzkrdyR064eSOp7HiR0RERJRLsOJHREREmsAxfjmPFT8iIiKiXIIVPyIiItIEnTWXc8lpTPyIiIhIE9ISPxUnd8CgWluWgl29RERERLkEK35ERESkCZzckfNY8SMiIiICcP36dXzwwQcoUqQI7O3tERAQgCNHjijPiwjCwsLg7u4OOzs71KlTB6dOnTJhxNnHip+Z0Bf0AhwdTR2GKr5wLG/qEFRV8+geU4egqjqeBU0dgqpsVKweaEV8qmUtSrvp/H+mDkFVpQrbmzoE1TyMf/xaz6fT6aCzUnFyhyHrbcXGxqJ69eqoW7cuNm7ciGLFiuHChQsoWLCgcswPP/yACRMmYN68efD29sZ3332HBg0aIDo6Gg4ODqrFnZOY+BEREZEmWFlbwUrFyR1WkvW2xo4dCw8PD8ydO1fZ5+npqfy/iGDixIn48ssv0apVKwDA/Pnz4eLigkWLFqFv376qxZ2T2NVLREREFk2v1xttiYmJGY5Zs2YNgoOD0aZNGxQrVgyVKlXCL7/8ojx/8eJFxMTEoGHDhso+W1tb1K5dG/v27Xst16EGJn5ERESkCenr+Km5AYCHhwecnJyULTw8PMO5//33X0yfPh1lypTBpk2bEBoaioEDB+K3334DAMTExAAAXFxcjF7n4uKiPGcO2NVLREREFu3q1atwfGKcvK2tbYZjDAYDgoODMWbMGABApUqVcOrUKUyfPh1dunRRjtPpjMcNikiGfVrGih8RERFpQk5V/BwdHY22ZyV+bm5uKF/eePJhuXLlcOXKFQCAq6srAGSo7t2+fTtDFVDLmPgRERFRrle9enVER0cb7Tt37hxKliwJAPDy8oKrqyu2bNmiPJ+UlISIiAhUq1bttcb6KtjVS0RERJpgylm9H3/8MapVq4YxY8agbdu2OHjwIGbNmoVZs2YBSOviHTx4MMaMGYMyZcqgTJkyGDNmDOzt7dGxY0fVYs5pTPyIiIhIG57onlWFZL2typUrY+XKlRg+fDhGjRoFLy8vTJw4EZ06dVKO+fTTT/Ho0SP069cPsbGxqFKlCjZv3mw2a/gBTPyIiIiIAADNmjVDs2bNnvu8TqdDWFgYwsLCXl9QKmPiR0RERJpgpdPBSsU7d1iZ0Wzb14WTO4iIiIhyCVb8iIiISBN01lbQqTi5Q2dgfetpfEeIiIiIcglW/IiIiEgTrKx1sFJxVq+VgWP8nsbEj4iIiDRBp/JyLjomfhmwq5eIiIgol2DFj4iIiDSBkztyHt8RIiIiolyCFT8iIiLSBCtrqDy5Q7WmLAYTPyIiItIEnZUOOhXv3KFmW5aCXb1EREREuQQrfkRERKQJVlZWsFJxcodVKutbTzPLd8TT0xM+Pj4ICAhQttOnT2f6moCAADx69AgAMHHiRNy+fVt5LiwsDMOGDct2HE++bs2aNfjkk0+y9RoiIiKi18lsK37Lly9HhQoVsnx8VFSU8v8TJ05E/fr1UaxYMdXiadGiBVq0aKFae0RERLmN6gs4q9iWpTDLit/z6HQ6hIWFoXr16vD29sbixYuNnnvw4AFGjRqFGzduoHXr1ggICFASwhs3bqB58+YoX7486tWrh3v37imvHT9+PEJCQhAYGIh33nkHV69ezXDuefPmoXXr1gCAmJgY1K1bF0FBQfD19cXAgQMhIjl78UREREQvYLaJX3rilr4lJSUBSEvw9u7di7/++gsfffRRhiRtxIgRcHd3x/LlyxEVFYWAgAAAwIEDBzB//nycPn0axYoVw8yZMwEAixYtwrlz5xAZGYmjR4+iQ4cOGDBgQKaxFSxYEGvXrsWRI0dw/Phx/Pvvv1ixYkWWrisxMRF6vd5oIyIiyg3SF3BWcyNjFtfV26tXLwBAqVKlUKNGDezevRsdO3Z8YXtNmjRB4cKFAQBVq1bFiRMnAACrVq3C4cOHERQUBABITU2FtbV1pm0ZDAZ89tln2LNnD0QEt2/fRkBAgFIRzEx4eDhGjhz5wuOIiIgsjc7KCjorFe/coWJblsJsE7+s0umy1r+fL18+5f+tra2RkpICABARfPXVV+jRo0eWzzlhwgTcvXsXBw4cQL58+TBkyBA8fvw4S68dPnw4hgwZojzW6/Xw8PDI8rmJiIiInsfiUuE5c+YAAC5duoQ9e/agRo0aGY5xdHREXFxcltpr0aIFpk2bpoz5S05OxrFjxzJ9TWxsLFxdXZEvXz7cunULy5Yty3L8tra2cHR0NNqIiIhyAytrK9U3Mma2Fb/WrVsbVekmT54MIC1xql69Ov777z9Mnjz5mdWygQMHonv37rC3t8e8efMyPU/nzp1x9+5d1KlTBzqdDikpKejZsycqVar03NcMHDgQbdq0QUBAAN544w3Ur1//5S6SiIiISEU6saDppjqdDvHx8ShQoICpQ1GNXq+Hk5MTLl67aTHVvy8cy5s6BFXVPLrH1CGoqo5nQVOHoCobC1zOISnVYn5tAwA2nb/34oPMSKnC9qYOQTUP4+PRrFIpxMXF5eh3UPp33dHeLeFgk1e1duOTkhH4y6ocj9+csAZKRERElEuYbVfvs1hQ8ZKIiCjX0VmpuwQLZ/VmZFGJHxEREZkvLueS8/iOEBEREeUSrPgRERGRJqTdbSPzmyRkr71U1dqyFKz4EREREeUSrPgRERGRJqh9f13eqzcjJn5ERESkCVZWVrBScUKGmm1ZCr4jRERERLkEK35ERESkCezqzXl8R4iIiIhyCVb8iIiISBNY8ct5fEeIiIiIcglW/IiIiEgTdDqVb9mmY33raUz8iIiISBPY1Zvz+I4QERER5RKs+BEREZEmsOKX8/iOEBEREeUSTPyIiIhIE6ysrVTfXlZ4eDh0Oh0GDx6s7BMRhIWFwd3dHXZ2dqhTpw5OnTqlwpW/Pkz8iIiIiJ5w6NAhzJo1C/7+/kb7f/jhB0yYMAFTpkzBoUOH4OrqigYNGiA+Pt5EkWYfx/jRa1fz6B5Th6Cqlud+N3UIqspboqupQ1DV9VQHU4eguoL5rE0dgqo6lbEzdQiq+ut6sqlDUE1C4uu9Fp2VTt3lXKx02X7NgwcP0KlTJ/zyyy/47rvvlP0igokTJ+LLL79Eq1atAADz58+Hi4sLFi1ahL59+6oWd05ixY+IiIg0IX1yh5obAOj1eqMtMTHxuTH0798fTZs2Rf369Y32X7x4ETExMWjYsKGyz9bWFrVr18a+ffty5g3JAUz8iIiIyKJ5eHjAyclJ2cLDw5953JIlS3D06NFnPh8TEwMAcHFxMdrv4uKiPGcO2NVLREREmpBTy7lcvXoVjo6Oyn5bW9sMx169ehWDBg3C5s2bkS9fvue3qTPuPhaRDPu0jIkfERERWTRHR0ejxO9Zjhw5gtu3byMoKEjZl5qail27dmHKlCmIjo4GkFb5c3NzU465fft2hiqglrGrl4iIiDQh/V69qm3ZuFfv22+/jRMnTiAqKkrZgoOD0alTJ0RFRaFUqVJwdXXFli1blNckJSUhIiIC1apVy4m3I0ew4kdERESaoLO2hpW1erPWddloy8HBARUqVDDalz9/fhQpUkTZP3jwYIwZMwZlypRBmTJlMGbMGNjb26Njx46qxZzTmPgRERERZcGnn36KR48eoV+/foiNjUWVKlWwefNmODiYz7JRTPyIiIhIE7R2r96dO3cat6fTISwsDGFhYa/UrilxjB8RERFRLsGKHxEREWmC1ip+lojvCBEREVEuwYofERERaUL6MixqtkfGmPgRERGRJrCrN+fxHSEiIiLKJVjxIyIiIk3QWenUrfhZmc89dF8XVvyIiIiIcglW/IiIiEgTOLkj5/EdISIiIsolWPEjIiIiTdBZWUNnZa1qe2SMiR8RERFpg5V12qZme2SEXb1EREREuQQrfkRERKQNVlZpm5rtkRG+I0RERES5hNkkfp6envDx8UFAQAACAgJQqlQpfPLJJwCAnTt3Ijg4+JXPcenSJTg7O2f7uazYuXMnNm/e/NKvJyIisnQ6a2vVNzJmVl29y5cvR4UKFV6pjZSUFOTJ8/ove+fOnXjw4AEaNmz42s9NRERkFji5I8eZTcXvafPmzUPr1q2Vx8nJyejevTuCgoIQHByMv//+G0BawhUQEICBAweiatWqWLlyJQ4fPoyqVavC398fISEh2Lt3r1Hbw4YNQ5UqVeDr64vt27c/8/wffPABgoOD4e/vj2bNmuH27dsAgH/++QfVq1dHxYoV4efnh6+++gpRUVGYMWMGfvvtNwQEBGDUqFHPva7ExETo9XqjjYiIiEgNZlXxa926NfLlywcA6Nq1q9Fzx48fx88//4w6dergjz/+QMeOHXHq1CnluSlTpmDSpElISkpC6dKl8csvv6BRo0bYs2cPWrdujfPnzwMA7t69Cz8/P4wfPx779+9Hy5YtceHChQyxTJw4Uen6/f777zFq1ChMmTIFU6ZMQdOmTfHFF18AAO7du4fChQsjNDQUDx48wPjx4zO9xvDwcIwcOfLV3igiIiJzZGWlcsXPbOtbOcas3pHly5cjKioKUVFRKFSokNFzpUuXRp06dQAAbdu2xfXr13Hjxg0AgLe3N2rUqAEAiI6Oho2NDRo1agQAqFGjBooVK4bjx48DAGxsbNC5c2cAwFtvvQVXV1elevikhQsXIjg4GH5+fpg9ezaioqIAALVq1cLs2bPx5ZdfYvPmzShYsGC2rnH48OGIi4tTtqtXr2br9URERETPY1aJX3bpdDoAQIECBZR9IqLsf9axmbWTbs+ePZgyZQo2btyIEydOYMKECXj8+DEA4P3338fevXtRtmxZTJkyBc2aNctWzLa2tnB0dDTaiIiIcoP0e/WquZExi3lHzp8/j127dgFIqwy+8cYbcHNzy3Ccj48PEhMTlbF7+/btw+3bt+Hn5wcASEpKwsKFCwEABw8eRExMDPz9/Y3aiI2NhaOjIwoXLoykpCTMnDlTee6ff/5BsWLF0KVLF/zwww/Yv38/AMDR0RFxcXHqXzgRERFRFpnVGL/MBAQEYMmSJRgyZAhEBIsWLXrmcTY2NlixYgUGDhyIhw8fIl++fFi2bBny58+P//77D0WKFMH58+dRpUoVPHjwAIsWLVKeS9ekSRMsWLAAPj4+KF68OKpVq4ZNmzYBAJYtW4aFCxfCxsYGIoIZM2YAAN577z38/vvvCAgIQKtWrTBixIicf1OIiIjMiU7lWb06zup9mk5ExNRB0PPp9Xo4OTnh4rWbFtPtu/H8PVOHoKqW5343dQiqylu/64sPMiPXxcHUIaiuYD7L+jKzT3lo6hBU9df1FFOHoJqEB/Ho+FZZxMXF5eh3UPp33a0/J8Mxv5167T58BJdWH+V4/ObEYrp6iYiIiChzFtPVS0REROZN7QkZnNyREd8RIiIiolyCFT8iIiLSBt6yLcex4kdERESUS7DiR0RERNrAW7blOCZ+REREpAk6a2vorNVL/NRsy1IwFSYiIiLKJVjxIyIiIm2wslK3e5ZdvRnwHSEiIiLKJVjxIyIiIm3gci45jokfERERaYLOyho6FZM1NduyFOzqJSIiIsolWPEjIiIibdCpPLlDx/rW0/iOEBEREeUSrPgRERGRJnCMX85jxY+IiIgol2DiR0RERNqQfq9e1baspznh4eGoXLkyHBwcUKxYMbRs2RLR0dFGx4gIwsLC4O7uDjs7O9SpUwenTp1S+13IUUz8iIiISBvS79yh5pZFERER6N+/P/bv348tW7YgJSUFDRs2xMOHD5VjfvjhB0yYMAFTpkzBoUOH4OrqigYNGiA+Pj4n3o0coRMRMXUQ9Hx6vR5OTk64fjMGjo6Opg5HFXGJqaYOQVVFDXpTh6CqPLFXTB2CqpLc/UwdguoSUnWmDkFVD5MNpg6BniNer4ev1xuIi4vL0e+g9O+6u3v+hGOB/Oq1++AhitRo9VLx//fffyhWrBgiIiJQq1YtiAjc3d0xePBgfPbZZwCAxMREuLi4YOzYsejbt69qceckVvyIiIhIE3TW1qpvQFpi+eSWmJj4wlji4uIAAIULFwYAXLx4ETExMWjYsKFyjK2tLWrXro19+/blwLuRM5j4ERERkUXz8PCAk5OTsoWHh2d6vIhgyJAhqFGjBipUqAAAiImJAQC4uLgYHevi4qI8Zw64nAsRERFpQw7dq/fq1atGXb22traZvmzAgAE4fvw49uzZk+E5nc54qIWIZNinZUz8iIiIyKI5OjpmeYzfRx99hDVr1mDXrl0oXry4st/V1RVAWuXPzc1N2X/79u0MVUAtY1cvERERaYOqS7lkr3ooIhgwYAD+/PNPbN++HV5eXkbPe3l5wdXVFVu2bFH2JSUlISIiAtWqVVPtLchprPgRERGRJuisrKBT8V692Wmrf//+WLRoEVavXg0HBwdl3J6TkxPs7Oyg0+kwePBgjBkzBmXKlEGZMmUwZswY2Nvbo2PHjqrFnNOY+BEREVGuN336dABAnTp1jPbPnTsX3bp1AwB8+umnePToEfr164fY2FhUqVIFmzdvhoODw2uO9uUx8SMiIiJt0Kk8uUOXva7eFzan0yEsLAxhYWGvEJRpcYwfERERUS7Bih8RERFpg04H6FSsSZnRMiuvCyt+RERERLkEK35ERESkDTorlSt+rG89jYkfERERaYLorCAqJmtqtmUp+I4QERER5RKs+BEREZE2sKs3x/EdISIiIsolWPEjIiIibdDp1F2Chcu5ZMDEj4iIiLTByiptU7M9MsJ3hIiIiCiXYMWPiIiINIHLueQ8viNEREREuQQrfkRERKQNXM4lx/EdISIiIsolWPEjIiIibWDFL8dp7h3x9PTEyZMnTR2GIiwsDElJScrjbt26YcqUKSaMiIiIyEKlJ35qbmSE78gLjBw50ijxU0NKSspzn0tMTIRerzfaiIiIiNSg2cSvTp06+OSTT1CrVi14eHhg3LhxWLJkCapVq4aSJUtiyZIlyrE6nQ7h4eEICQlBqVKlsHXrVgwfPhyVKlWCr68vTp06lem5zp8/j/r168Pf3x8BAQFYtWoVACA0NBQAUK1aNQQEBOD27dsAgNOnT6N+/frw9vZGq1atlMQwOTkZn3/+OUJCQhAQEID27dvj/v37ANIqhQMHDkTjxo1RsWLF58YSHh4OJycnZfPw8HjZt5CIiMisiE6nLOmizsY7dzxNs4kfAFy5cgU7d+7EgQMHMGLECJw8eRL79u3DsmXLMGTIEKNjHR0dcfDgQYwdOxbvvvsuatSogWPHjqFr164YPXp0pufp1KkT2rZti+PHj2PZsmXo2bMnrl69ihkzZgAA9u3bh6ioKBQrVgwAEBUVhbVr1+LMmTO4desWVqxYAQAYN24cChQogIMHDyIqKgq+vr745ptvlPPs2bMHy5cvzzQRHT58OOLi4pTt6tWrL/XeERERET1N05M72rRpAysrK7i7u8PZ2RktW7YEAAQFBeHmzZt4/Pgx8uXLBwBo164dACAwMBBWVlZo2rSpcuyff/753HPEx8cjKioKPXv2BACUKVMGNWrUwJ49e9ChQ4dnvqZVq1aws7MDAISEhODChQsAgFWrVkGv12P58uUAgKSkJLz55pvK69q2bYsCBQpkes22trawtbXN9BgiIiKLxMkdOU7TiV96UgcA1tbWymNra2sAxmPlnnzuycTJ2to60zF1IgIgrbv4SU8/ziyu9PZFBNOmTUO9evWe+boXJX1EREREOSnXp8KOjo4ICAjA/PnzAQAXLlzA3r17Ub16dQCAg4MD4uListRWixYtMGHCBCQkJAAAEhISXji+kIiIiP5Hp1N/IyO5PvEDgIULF2LBggWoWLEi3n//fcyePVuZVDF06FDUq1fPaHLH83z++ecICAhAlSpV4O/vj7feegtRUVGv4QqIiIgsAJdzyXE6Se/rJE3S6/VwcnLC9ZsxcHR0NHU4qohLTDV1CKoqarCsJXfyxF4xdQiqSnL3M3UIqktItawqxsNkg6lDoOeI1+vh6/UG4uLicvQ7KP277s6Fk3B0cFCv3fh4OL9ZIcfjNyeaHuNHREREuUf6MixqtkfGcs07smHDBgQEBGTYli5daurQiIiIiF6LXFPxe+edd/DOO++YOgwiIiJ6Hp0VYMXlXHJSrkn8iIiISOO4jl+O4ztCRERElEuw4kdERETawIpfjuM7QkRERJRLsOJHRERE2sCKX47jO0JERESUS7DiR0RERJogOp3KCzhb1l1u1MDEj4iIiLSBXb05ju8IERERUS7Bih8RERFpg06XtqnZHhlhxY+IiIgol2DFj4iIiLSBY/xyHN8RIiIiov+ZNm0avLy8kC9fPgQFBWH37t2mDklVTPyIiIhIE0RnpfqWHUuXLsXgwYPx5Zdf4tixY6hZsyaaNGmCK1eu5NAVv35M/IiIiEgb0rt61dyyYcKECejZsyd69eqFcuXKYeLEifDw8MD06dNz6IIzV69ePYwcOTLD/tjYWNSrV++l2uQYPzNhkLTNEthYW9Ysq+upDqYOQVVu7n6mDkFVVo/iTB2C6uztC5k6BFUVSH5g6hBUZUmLBufTJZg6BFXo9Xqjx7a2trC1tTXal5SUhCNHjuDzzz832t+wYUPs27cvx2N8lp07d+LEiRM4duwYFi5ciPz58wNIizUiIuKl2mTFj4iIiDQh7c4d6m4A4OHhAScnJ2ULDw/PcO47d+4gNTUVLi4uRvtdXFwQExPzWq7/WbZu3YqYmBi89dZbuHTp0iu3x4ofERERWbSrV6/C0dFRefx0te9JuqcqtiKSYd/r5ObmhoiICPTo0QOVK1fGsmXLUK5cuZduj4kfERERaYJI2qZmewDg6OholPg9i7OzM6ytrTNU927fvp2hCvi6pCectra2WLhwIb777js0btwYn3322Uu3ya5eIiIi0gSDiOpbVtnY2CAoKAhbtmwx2r9lyxZUq1ZN7UvNEnkq/q+++goLFy7Ejz/++NJtsuJHREREBGDIkCHo3LkzgoODUbVqVcyaNQtXrlxBaGioSeK5ePEiihYtarTv/fffh4+PDw4fPvxSbTLxIyIiIk2Q/21qtpcd7dq1w927dzFq1CjcvHkTFSpUwIYNG1CyZEkVo8q6553X19cXvr6+L9UmEz8iIiKi/+nXrx/69etn6jByDBM/IiIi0gS116y1lPVv1cTJHURERES5BCt+REREpAkikmEm66u2R8aY+BEREZEmsKs357Grl4iIiCiXYMWPiIiININFupzFih8RERFRLsGKHxEREWkCx/jlPFb8iIiIiHIJVvyIiIhIE7icS85j4kdERESaYPjfpmZ7ZIxdvURERES5BCt+REREpAkiaZua7ZExVvyIiIiIcglW/IiIiEgTuJxLzmPiR0RERJrAWb05j129RERERLlEthI/T09PFCtWDMnJycq+7du3Q6fTYdiwYS8dhKenJ06ePPnC4wICAvDo0aOXbuPChQto06YNvLy84Ofnh8DAQMyePRsAEBYWhmLFiqFSpUooW7YsKleujEmTJiE1NVV5fevWreHu7g6dTocHDx4YtT1w4EB4enpCp9Nl6VqIiIjImCEHNjKW7YpfiRIlsGbNGuXxnDlzEBwcrGpQzxMVFQU7O7uXem1MTAxq1KiBhg0b4uLFizhx4gS2bt2KlJQU5ZguXbrg2LFjiI6OxrJly/DHH3/g448/Vp4PDQ1FVFTUM9tv3bo19uzZg5IlS75UfOkSExOh1+uNNiIiIiI1ZDvx69GjB+bMmQMAiIuLw/79+9G4cWMAgJ+fHyIjI5VjZ86ciXbt2gFIS7zatm2LkJAQ+Pv7Y8SIEc9s//z586hfvz78/f0REBCAVatWKc89WWnbvXs3/Pz8EBISggEDBrywH3/q1KmoWbMmevfurewrXLgwQkNDn3m8p6cn5syZg+nTpyMuLg4AUL9+fRQrVuyZx9eqVQvFixd/5nNnzpxBo0aN4O/vD39/f8yYMeO5cYaHh8PJyUnZPDw8Mr0uIiIiSyH4/yVdVNlMfUEalO3Er1atWvj3339x/fp1LF68GG3atIG1tTWAtO7OqVOnKsdOnToVAwYMAAB07doVAwYMwMGDB3H06FEcPHgQK1euzNB+p06d0LZtWxw/fhzLli1Dz549cfXqVaNjEhMT0b59e0yePBkHDx5ErVq1cOXKlUzjPnLkCKpWrZqta/X29oa9vT2io6Oz9bonpaSk4N1330XPnj1x/PhxHD9+HK1bt37u8cOHD0dcXJyyPX3tRERERC/rpWb1du7cGfPnz8eqVauwcOFCLFy4EADwwQcf4JtvvsHt27dx5swZ6HQ61KxZEw8fPsT27dtx69YtpY0HDx7g7NmzRu3Gx8cjKioKPXv2BACUKVMGNWrUwJ49e9ChQwfluOjoaNjb26NOnToAgLZt26JPnz4vcyk5Ljo6GikpKWjbtq2yz9nZ+bnH29rawtbW9nWERkREpCkGERhUnImrZluW4qUSv27duiEwMBDe3t4oU6aMst/Ozg5du3bF7NmzcezYMaXaZzAYoNPpcOjQIeTNm/e57aZ31+p0OqP9Tz9+menZQUFBiIyMNBqz9yLR0dFISEiAj49Pts9HRERE2SNQt3uWaV9GL7Wci7u7O8LDwzF27NgMz/Xv3x/Tp09HREQEOnXqBABwcHBAzZo18f333yvH3bhxA9euXTN6raOjIwICAjB//nwAabNw9+7di+rVqxsd5+Pjg0ePHmHXrl0AgOXLlyvj8J6nX79+iIiIwNy5c5V99+7dw8SJE595/KVLl9CzZ098+OGHcHR0zLTtzJQtWxY2NjZYtmyZsu/OnTsv3R4RERHRy3rpdfy6d+/+zDFzxYsXR0BAADp37gx7e3tl/8KFC3HmzBn4+fnBz88P77//Pu7evZvh9QsXLsSCBQtQsWJFvP/++5g9e3aGCQ62trZYvHgx+vfvj5CQEBw8eBAlSpTINF43Nzfs2bMH69atg5eXF/z9/dGgQQOjpO63335TlnNp06YNWrdujZ9++kl5vkWLFsoEjrJlyypdzUBawlu8eHFcu3YN9evXR+nSpQEAefLkwerVqzFr1iz4+fnB398fK1asyDRWIiKi3Cj9zh1qbmRMJyova/3gwQP4+Phg9+7d8PLyUrPpXEmv18PJyQlXb8S8UuVRSxJTLWtlpQdJlnU9bvaWta671aPMewPMkcG+kKlDUJWlfUby1PAkc6bXx6OYZxnExcXl6HdQ+nfd0QvX4OCg3nni4/UIfLN4jsdvTlT9DT9jxgz4+PigX79+TPqIiIgoe9RcykXtAYMWQtV79YaGhj53XbzXJTg42GhRZgDw9fVVZh4TERER5VaqJn5acPjwYVOHQERERC/BAIFBxTKdmm1ZCotL/IiIiMg8KV20KrZHxixrFDcRERERPRcrfkRERKQJai/BwuVcMmLFj4iIiCiXYMWPiIiINIFj/HIeEz8iIiLSBM7qzXns6iUiIiLKJVjxIyIiIk1gV2/OY8WPiIiIKJdgxY+IiIg0wSACg4plOjXbshSs+BERERHlEkz8iIiISBNSDepvOeHSpUvo2bMnvLy8YGdnhzfffBPffPMNkpKSjI67cuUKmjdvjvz588PZ2RkDBw7McMzrxq5eIiIi0gRz6eo9e/YsDAYDZs6cidKlS+PkyZPo3bs3Hj58iPHjxwMAUlNT0bRpUxQtWhR79uzB3bt30bVrV4gIJk+enCNxZQUTPyIiIqJsaNy4MRo3bqw8LlWqFKKjozF9+nQl8du8eTNOnz6Nq1evwt3dHQDw448/olu3bhg9ejQcHR1NEju7eomIiEgTDCJIVXFLr/jp9XqjLTExUfXY4+LiULhwYeVxZGQkKlSooCR9ANCoUSMkJibiyJEjqp8/q5j4ERERkUXz8PCAk5OTsoWHh6va/oULFzB58mSEhoYq+2JiYuDi4mJ0XKFChWBjY4OYmBhVz58d7Oo1EzY6A2x0OTRK9TWLT7Ws6fUF81mbOgRVJaSaOgJ12dsXMnUIqrO+d9nUIajK4Ojy4oPMiFjbmDoE1Yjt6/3eMYi64/IM/2vq6tWrRl2rtra2zzw+LCwMI0eOzLTNQ4cOITg4WHl848YNNG7cGG3atEGvXr2MjtXpdBleLyLP3P+6MPEjIiIii+bo6JilMXUDBgxA+/btMz3G09NT+f8bN26gbt26qFq1KmbNmmV0nKurKw4cOGC0LzY2FsnJyRkqga8TEz8iIiLSBLWXYMluW87OznB2ds7SsdevX0fdunURFBSEuXPnwsrKePRc1apVMXr0aNy8eRNubm4A0iZ82NraIigoKHuBqYiJHxEREWmCuSzncuPGDdSpUwclSpTA+PHj8d9//ynPubq6AgAaNmyI8uXLo3Pnzhg3bhzu3buHYcOGoXfv3iab0Qsw8SMiIiLKls2bN+P8+fM4f/48ihcvbvSc/C/ZtLa2xvr169GvXz9Ur14ddnZ26Nixo7Lci6kw8SMiIiJNSF+GRc32ckK3bt3QrVu3Fx5XokQJrFu3LkdieFlczoWIiIgol2DFj4iIiDTBgP9fgkWt9sgYEz8iIiLShFSDIFXFzE/NtiwFu3qJiIiIcglW/IiIiEgTROXlXCSHJneYM1b8iIiIiHIJVvyIiIhIE1IlbVOzPTLGih8RERFRLsGKHxEREWmCudyyzZwx8SMiIiJN4HIuOY9dvURERES5BCt+REREpAns6s15rPgRERER5RKs+BEREZEmcDmXnMeKHxEREVEuwYofERERaQLH+OU8Jn5ERESkCQaDwKDiEixqtmUp2NVLRERElEuokvh5enqiWLFiSE5OVvZt374dOp0Ow4YNe6V2T548+cLjAgIC8OjRo5du48KFC2jTpg28vLzg5+eHwMBAzJ49GwAQFhaGYsWKoVKlSihbtiwqV66MSZMmITU1VXl9bGwsOnXqhDJlyqBcuXL4/PPPlec2bdqEoKAgVKpUCRUqVMD8+fOzculERES5jkH+f4KHGhsLfhmpVvErUaIE1qxZozyeM2cOgoOD1Wo+U1FRUbCzs3up18bExKBGjRpo2LAhLl68iBMnTmDr1q1ISUlRjunSpQuOHTuG6OhoLFu2DH/88Qc+/vhj5fkePXqgUqVK+Oeff3DmzBkMGjQIACAi6NixI+bOnYtjx45h3bp16Nu3L+Lj458bT2JiIvR6vdFGREREpAbVEr8ePXpgzpw5AIC4uDjs378fjRs3BgD4+fkhMjJSOXbmzJlo164dgLTEq23btggJCYG/vz9GjBjxzPbPnz+P+vXrw9/fHwEBAVi1apXynE6nw4MHDwAAu3fvhp+fH0JCQjBgwADICwZ2Tp06FTVr1kTv3r2VfYULF0ZoaOgzj/f09MScOXMwffp0xMXF4fz58zh69CiGDBmiHOPm5mb0mvv37wMA9Ho9ihQpAltb2+fGEx4eDicnJ2Xz8PDINH4iIiJLkT65Q82NjKmW+NWqVQv//vsvrl+/jsWLF6NNmzawtrYGAAwcOBBTp05Vjp06dSoGDBgAAOjatSsGDBiAgwcP4ujRozh48CBWrlyZof1OnTqhbdu2OH78OJYtW4aePXvi6tWrRsckJiaiffv2mDx5Mg4ePIhatWrhypUrmcZ95MgRVK1aNVvX6u3tDXt7e0RHR+P06dPw8PBAaGgoAgMD0bBhQxw7dgxAWkL6xx9/oFWrVihZsiRq1KiB+fPnw8bG5rltDx8+HHFxccr29DUSERFZqlQR1Tcypurkjs6dO2P+/PmYM2cOevTooez/4IMPsH37dty+fRsRERHQ6XSoWbMmHj58iO3bt2PgwIEICAhAcHAwzp8/j7Nnzxq1Gx8fj6ioKPTs2RMAUKZMGdSoUQN79uwxOi46Ohr29vaoU6cOAKBt27ZwcnJS8xIzSE5ORmRkJDp06ICjR49i6NChaN68OVJSUpCSkoLw8HCsXr0aly9fxrZt29C1a1fcu3fvue3Z2trC0dHRaCMiIiJSg6rLuXTr1g2BgYHw9vZGmTJllP12dnbo2rUrZs+ejWPHjinVPoPBAJ1Oh0OHDiFv3rzPbTe9u1an0xntf/rxi7p1nyUoKAiRkZFGY/ZeJDo6GgkJCfDx8YGVlRXeeOMN1K1bFwDQqFEjJCUl4dq1a7hz5w5u3LiB6tWrAwAqV64Md3d3/P3338rxRERElIbLueQ8VSt+7u7uCA8Px9ixYzM8179/f0yfPh0RERHo1KkTAMDBwQE1a9bE999/rxx348YNXLt2zei1jo6OCAgIUGbEXrhwAXv37lUSqnQ+Pj549OgRdu3aBQBYvnw54uLiMo25X79+iIiIwNy5c5V99+7dw8SJE595/KVLl9CzZ098+OGHcHR0RFBQEBwdHXH8+HEAwOHDhwEAb7zxBjw8PHDt2jVER0cDSBuneOHCBXh7e2caExEREVFOUH0B5+7duz9zf/HixREQEKCMj0u3cOFCDBkyBH5+fgCAAgUKYMaMGShevLjR6xcuXIi+ffti4sSJ0Ol0mD17doaJD7a2tli8eDH69esHOzs71KlTByVKlMg0Xjc3N+zZsweff/45Ro0aBQcHB+TNmxf9+/dXjvntt9+wbds2JCQkwNHREZ06dcJHH30EIK3qOG/ePPTq1QuPHz9Gvnz5sGLFCuTNmxcuLi6YOXMmWrduDSsrK4gIpk2bhjfeeCPrbygREVEukQqV79WrXlMWQycv0z/6Eh48eAAfHx/s3r0bXl5er+OUFkGv18PJyQm3bt6wmPF+dxMtq/Run9ey1kG3tLHQ9nl0Lz7IzFjfu2zqEFRlcHQxdQiqEuvnT+AzN3q9Hi5u7oiLi8vR76D077qftp2AXX4H1dp99DAeH7/tl+Pxm5PX8o01Y8YM+Pj4oF+/fkz6iIiI6Jm4nEvOey336g0NDX3uunivS3BwsNGizADg6+uLhQsXmigiIiIiepLaS7BwOZeMXkvipwXpky6IiIiIcqtck/gRERGRthkMglQu55KjLGtUOhERERE9Fyt+REREpAmpKlf81GzLUrDiR0RERJRLsOJHREREmsCKX85j4kdERESakGpQN1lLNajWlMVgVy8RERFRLsGKHxEREWkCu3pzHit+RERERLkEK35ERESkCaz45TwmfkRERKQJvHNHzmNXLxEREdFLSkxMREBAAHQ6HaKiooyeu3LlCpo3b478+fPD2dkZAwcORFJSkmkC/R9W/IiIiEgTUkXlrl7J+Yrfp59+Cnd3d/z999/G505NRdOmTVG0aFHs2bMHd+/eRdeuXSEimDx5co7H9Tys+BERERG9hI0bN2Lz5s0YP358huc2b96M06dPY8GCBahUqRLq16+PH3/8Eb/88gv0er0Jok3Dih8RERFpQk5N7ng60bK1tYWtre0rtX3r1i307t0bq1atgr29fYbnIyMjUaFCBbi7uyv7GjVqhMTERBw5cgR169Z9pfO/LFb8iIiIyKJ5eHjAyclJ2cLDw1+pPRFBt27dEBoaiuDg4GceExMTAxcXF6N9hQoVgo2NDWJiYl7p/K+CFT8zIToriM4y8vRN5/8zdQiq6lTGztQhqOq2wbKup0DyA1OHoDqDo8uLDzIjuuREU4egKt2jOFOHoBqrh/Gv9Xw5VfG7evUqHB0dlf3Pq/aFhYVh5MiRmbZ56NAh7Nu3D3q9HsOHD8/0WJ1Ol2GfiDxz/+vCxI+IiIg0IcUgsFYx8Uv5X1uOjo5Gid/zDBgwAO3bt8/0GE9PT3z33XfYv39/hgQyODgYnTp1wvz58+Hq6ooDBw4YPR8bG4vk5OQMlcDXiYkfEREREQBnZ2c4Ozu/8LhJkybhu+++Ux7fuHEDjRo1wtKlS1GlShUAQNWqVTF69GjcvHkTbm5uANImfNja2iIoKChnLiALmPgRERGRJpjLnTtKlChh9LhAgQIAgDfffBPFixcHADRs2BDly5dH586dMW7cONy7dw/Dhg1D7969s1R9zCmWMWiMiIiISEOsra2xfv165MuXD9WrV0fbtm3RsmXLZy798jqx4kdERESaYK63bPP09IQ8Y7HoEiVKYN26da8lhqxixY+IiIgol2DFj4iIiDQhVUTV26y9jlu2mRsmfkRERKQJ5jK5w5yxq5eIiIgol2DFj4iIiDSBFb+cx4ofERERUS7Bih8RERFpAit+OY8VPyIiIqJcghU/IiIi0oRUMSDVYFC1PTLGxI+IiIg0wVzv3GFO2NVLRERElEuw4kdERESakGoQWHFyR45ixY+IiIgol2DFj4iIiDQhxQDoVKzSpXBuRwZM/IiIiEgT2NWb89jVS0RERJRLsOJHREREmsCKX85jxY+IiIgolzCbxC8gIAABAQEoX7488uTJozxu165dttq5dOkSnJ2dlcc6nQ4PHjzI9DUXLlxAmzZt4OXlBT8/PwQGBmL27NkAgLCwMBQrVgyVKlVC2bJlUblyZUyaNAmpqakZ2unRo0eWzkdERJQbpd+rV82NjJlNV29UVBSAtMQtODhYeZzTYmJiUKNGDYwaNQrLli0DANy7dw9//PGHckyXLl0wfvx4Jb4PPvgA58+fx6RJk5Rj1q5dC51O98LzJSYmIjExUXms1+vVuhQiIiLK5cym4vc869evR+XKlVGxYkUEBATgwIEDAIBDhw6hXr16CA4ORmBgIFasWPFS7U+dOhU1a9ZE7969lX2FCxdGaGjoM4/39PTEnDlzMH36dMTFxQEA7t69i5EjR2LChAkvPF94eDicnJyUzcPD46XiJiIiMjcGlat9vGVbRmZT8XuWc+fOoWfPnti1axe8vb2RnJyMhIQE3L9/H3379sX69evh5uaGO3fuICgoCNWrV8/2OY4cOYIGDRpk6zXe3t6wt7dHdHQ0QkJC0L9/f4SFhcHJyemFrx0+fDiGDBmiPNbr9Uz+iIgoV0g1iKrr+LGrNyOzTvy2bNmCd955B97e3gCAvHnzwsnJCRs2bMC///6LJk2aKMeKCKKjo1GyZMnXGuOyZctgY2ODZs2aZel4W1tb2Nra5nBURERElBuZfVfvs4gI/P39ERUVpWxXrlxB7dq1s91WUFAQIiMjs/Wa6OhoJCQkwMfHBzt27MD27dvh6ekJT09PAICvry9OnDiR7ViIiIgsmYhADCpuworf08w68WvUqBE2btyIc+fOAQCSk5MRFxeHatWq4Z9//sH27duVY6OiopCUlJTtc/Tr1w8RERGYO3eusu/evXuYOHHiM4+/dOkSevbsiQ8//BCOjo6YNm0arl27hkuXLuHSpUsAgFOnTsHPzy/bsRARERG9CrNO/EqXLo1ff/0VHTp0gL+/P0JCQhAdHY1ChQph7dq1+Pbbb1GxYkWUL18en3/+OQyG7N+0z83NDXv27MG6devg5eUFf39/NGjQAI6Ojsoxv/32m7KcS5s2bdC6dWv89NNPal4qERGRxTP8b0KGmhsZ0wnroJqm1+vh5OSEmJgYo2TTnC0++Z+pQ1BVpzJ2pg5BVbcNlnU9LrC8dTMlr2WNA9YlJ774IHOS8tjUEahGHx+PoqX9ERcXl6PfQenfdTXCNyBPvvyqtZvy+CH2DH8nx+M3J2Y9uYOIiIgsh4i64/JY28qIid//BAcHIyUlxWifr68vFi5caKKIiIiIcpf0SRlqtkfGmPj9z+HDh00dAhEREVGOYuJHREREmqD2hAxO7sjIrGf1EhEREVHWseJHREREmiCGtE3N9sgYEz8iIiLSBM7qzXns6iUiIiLKJVjxIyIiIk3g5I6cx4ofERERUS7Bih8RERFpAhdwznms+BERERHlEqz4ERERkTaoXPEDK34ZsOJHREREmmAQUX3LSevXr0eVKlVgZ2cHZ2dntGrVyuj5K1euoHnz5sifPz+cnZ0xcOBAJCUl5WhML8KKHxEREVE2rVixAr1798aYMWNQr149iAhOnDihPJ+amoqmTZuiaNGi2LNnD+7evYuuXbtCRDB58mSTxc3Ej4iIiDRBROXJHTlU8UtJScGgQYMwbtw49OzZU9lftmxZ5f83b96M06dP4+rVq3B3dwcA/Pjjj+jWrRtGjx4NR0fHHIntRdjVS0RERBZNr9cbbYmJia/U3tGjR3H9+nVYWVmhUqVKcHNzQ5MmTXDq1CnlmMjISFSoUEFJ+gCgUaNGSExMxJEjR17p/K+CiR8RERFpQvpyLmpuAODh4QEnJydlCw8Pf6U4//33XwBAWFgYvvrqK6xbtw6FChVC7dq1ce/ePQBATEwMXFxcjF5XqFAh2NjYICYm5pXO/yqY+BEREZFFu3r1KuLi4pRt+PDhzzwuLCwMOp0u0+3w4cMwGAwAgC+//BLvv/8+goKCMHfuXOh0OixbtkxpT6fTZTiHiDxz/+vCMX5mQicG6MRg6jBUUaqwvalDUNVf15NNHYKqAt1MHYG6xIS/YHOKWNuYOgRV6R7FmToEVcnJXaYOQTXy8NFrPZ/BAOhUvWVb2n8dHR2zNKZuwIABaN++fabHeHp6Ij4+HgBQvnx5Zb+trS1KlSqFK1euAABcXV1x4MABo9fGxsYiOTk5QyXwdWLiR0RERJogIqpOyMhuW87OznB2dn7hcUFBQbC1tUV0dDRq1KgBAEhOTsalS5dQsmRJAEDVqlUxevRo3Lx5E25uaX9Rb968Gba2tggKCsrmlaiHiR8RERFRNjg6OiI0NBTffPMNPDw8ULJkSYwbNw4A0KZNGwBAw4YNUb58eXTu3Bnjxo3DvXv3MGzYMPTu3dtkM3oBJn5ERESkEWJI29RsL6eMGzcOefLkQefOnfHo0SNUqVIF27dvR6FChQAA1tbWWL9+Pfr164fq1avDzs4OHTt2xPjx43MuqCxg4kdERESUTXnz5sX48eMzTeRKlCiBdevWvcaoXoyJHxEREWmCwSAqT+7gvXqfxsSPiIiINOHJtffUao+McR0/IiIiolyCFT8iIiLSBFb8ch4rfkRERES5BCt+REREpAkGEehUXMDZoGJbloIVPyIiIqJcghU/IiIi0gSO8ct5TPyIiIhIE0RUTvzY1ZsBu3qJiIiIcglW/IiIiEgTxCCq3m2DXb0ZseJHRERElEuw4kdERESaICKqjsvjGL+MWPEjIiIiyiVY8SMiIiJN4HIuOY+JHxEREWmCwSCAismamhNFLAW7eomIiIhyCVb8iIiISBPEkAoxpKraHhljxY+IiIgolzCbxC8gIAABAQEoX7488uTJozxu165dttq5dOkSnJ2dlcc6nQ4PHjzI9DUXLlxAmzZt4OXlBT8/PwQGBmL27NkAgLCwMBQrVgyVKlVC2bJlUblyZUyaNAmpqf//V8aCBQvg7++PgIAAVKpUCRs3bsxWzERERLlBesVPzY2MmU1Xb1RUFIC0xC04OFh5nNNiYmJQo0YNjBo1CsuWLQMA3Lt3D3/88YdyTJcuXTB+/Hglvg8++ADnz5/HpEmTcO/ePfTr1w/R0dFwc3PDnj170KpVK9y+ffuZ50tMTERiYqLyWK/X5+DVERERaYcYDCp39RpUa8tSmE3F73nWr1+PypUro2LFiggICMCBAwcAAIcOHUK9evUQHByMwMBArFix4qXanzp1KmrWrInevXsr+woXLozQ0NBnHu/p6Yk5c+Zg+vTpiIuLg8FggIgoVcX79++jePHizz1feHg4nJyclM3Dw+Ol4iYiIiJ6mtlU/J7l3Llz6NmzJ3bt2gVvb28kJycjISEB9+/fR9++fbF+/Xq4ubnhzp07CAoKQvXq1bN9jiNHjqBBgwbZeo23tzfs7e0RHR2NkJAQzJgxA4GBgShcuDAePXqErVu3Pve1w4cPx5AhQ5THer2eyR8REeUKkpoKSVWx4qdiW5bCrBO/LVu24J133oG3tzcAIG/evHBycsKGDRvw77//okmTJsqxIoLo6GiULFnytcao1+sxbdo0HD58GGXLlsXatWvRunVrnD59GnnyZHz7bW1tYWtr+1pjJCIiotzB7Lt6n0VE4O/vj6ioKGW7cuUKateune22goKCEBkZma3XREdHIyEhAT4+Pti8eTOcnJxQtmxZAEDz5s0RGxuLq1evZjsWIiIiSyai8uQOYcXvaWad+DVq1AgbN27EuXPnAADJycmIi4tDtWrV8M8//2D79u3KsVFRUUhKSsr2Ofr164eIiAjMnTtX2Xfv3j1MnDjxmcdfunQJPXv2xIcffghHR0eUKlUKR48eVSZzREZGwmAw4I033sh2LERERESvwqy7ekuXLo1ff/0VHTp0QHJyMqytrTFz5kyEhIRg7dq1+OSTT/Dxxx8jOTkZJUqUwKpVq7J9jvSZuJ9//jlGjRoFBwcH5M2bF/3791eO+e2337Bt2zYkJCTA0dERnTp1wkcffQQACAwMxPDhw1GnTh3kzZsXefPmxR9//AEbGxu13gYiIiKLwAWcc55ORHgjOw3T6/VwcnLCrZs34OjoaOpwVLH/5iNTh6CquMfJpg5BVYFuDqYOQVXFdA9NHYLqxLaAqUNQldWD/0wdgqrk5C5Th6Aa/cNHKNYiFHFxcTn6HZT+Xefy3o+wymunWruG5Ee4tXJojsdvTsy6q5eIiIiIss6su3rVFBwcjJSUFKN9vr6+WLhwoYkiIiIiyl3Y1ZvzmPj9z+HDh00dAhEREVGOYuJHREREmsBbtuU8jvEjIiIiyiVY8SMiIiJNMBhSARUrfgaO8cuAiR8RERFpAid35Dx29RIRERHlEqz4ERERkSaw4pfzWPEjIiIiyiVY8SMiIiJtSE2FWKlYpUtlxe9pTPyIiIhIE0TUndUrwsTvaezqJSIiIsolmPgRERGRJqTfuUO9Lefu3HHu3Dm8++67cHZ2hqOjI6pXr44dO3YYHXPlyhU0b94c+fPnh7OzMwYOHIikpKQciykrmPgRERERZVPTpk2RkpKC7du348iRIwgICECzZs0QExMDAEhNTUXTpk3x8OFD7NmzB0uWLMGKFSswdOhQk8bNMX5ERESkCaLynTvSl3PR6/VG+21tbWFra/vS7d65cwfnz5/HnDlz4O/vDwD4/vvvMW3aNJw6dQqurq7YvHkzTp8+jatXr8Ld3R0A8OOPP6Jbt24YPXo0HB0dX/r8r4IVPyIiIrJoHh4ecHJyUrbw8PBXaq9IkSIoV64cfvvtNzx8+BApKSmYOXMmXFxcEBQUBACIjIxEhQoVlKQPABo1aoTExEQcOXLklc7/KljxIyIiIk0QgwFQcVxe+hi/q1evGlXYXqXaBwA6nQ5btmzBu+++CwcHB1hZWcHFxQV//fUXChYsCACIiYmBi4uL0esKFSoEGxsbpTvYFJj4aZyIAADi4+NNHIl6HsY/NnUIqkpITDZ1CKqK14upQ1BVPl2CqUNQndjm3IB1U7B6aDm/3wBAHj4ydQiqiU9Iu5b076KcllNdvY6OjlnqWg0LC8PIkSMzPebQoUMICgpCv379UKxYMezevRt2dnaYPXs2mjVrhkOHDsHNzQ1AWoKYISaRZ+5/XZj4aVx6wlfau6yJIyEiotwqPj4eTk5Opg4jxw0YMADt27fP9BhPT09s374d69atQ2xsrJJQTps2DVu2bMH8+fPx+eefw9XVFQcOHDB6bWxsLJKTkzNUAl8nJn4a5+7ujqtXr8LBwSFH/0LQ6/Xw8PDIUA43V7webeP1aBuvR/te1zWJCOLj443GqeWknKr4ZZWzszOcnZ1feFxCQlpPgpWV8VQJKysrGP7XvVy1alWMHj0aN2/eVCqAmzdvhq2trTIO0BSY+GmclZUVihcv/trOl9VyuLng9Wgbr0fbeD3a9zquKTdU+rKratWqKFSoELp27YoRI0bAzs4Ov/zyCy5evIimTZsCABo2bIjy5cujc+fOGDduHO7du4dhw4ahd+/eJv055KxeIiIi0gSDIVX1LSc4Ozvjr7/+woMHD1CvXj0EBwdjz549WL16NSpWrAgAsLa2xvr165EvXz5Ur14dbdu2RcuWLTF+/PgciSmrWPEjIiIiyqbg4GBs2rQp02NKlCiBdevWvaaIsoaJHwFIm9r+zTffvPIUd63g9Wgbr0fbeD3aZ4nXBACSagB0Ko7xS7WsGfBq0MnrmqNNRERE9Ax6vR5OTk6wDeoFnbWNau1KahISj8xGXFycxY3vfFkc40dERESUS7Crl4iIiDRBDKnqdvXm0OQOc8aKHxEREVEuwYofERERaQIrfjmPiR+ZNVPf85CIiNTDxC/nMfHLJW7evIn8+fNbzKwmvV4PR0dH6HQ6i0z+LPGaiHKawWDIcAstcyQiEBHl9l+WcE1ZlpoMVZcaSU1WszWLwMTPwokIrl+/jk6dOqF79+5o1aqV2Sd/9+/fx9dffw1vb2989NFHZp/8xcXFQURQsGBB7N+/H76+vnBwcDB1WKpI/1zM+fOxRCkpKbCysrKIxOLs2bOYOHEiZsyYYTHXM23aNMTGxuKLL75AuXLlTB3Sa2FjYwNXV1fEnP5D9bZdXV1hY6PeEjHmjomfhdPpdChevDjatm2LhQsXwsbGBs2aNTPr5M/Kygpubm74+++/MXv2bPTq1ctsk4vExETs3r0b0dHR0Ov1OHHiBObMmWPqsFRz7tw5lC1b1uw+F0t2+vRp/PTTT7h9+zZGjx6NChUqmDqklyYimDlzJmbNmoXY2FgsXbrUrJO/06dPo1u3bmjbti1SUlIwYMAAbNmyxayvKavy5cuHixcvIikpSfW2bWxskC9fPtXbNVdcwNnCPZkMzZgxA0uXLkXv3r3NNvlLv564uDjMmTMHUVFRqFmzJnr16mX0vDk5cOAA+vTpg7t372LRokWoVasWUlNTYW1tberQXsmOHTvQpUsXREREwMvLy+w+l3TpP1MHDhxAbGwsGjdubOqQXtqZM2fQrVs39OzZEydOnMDu3btx+PBh5MljvjWAHTt2ICoqCkuXLkX+/Pmxbds2iAgMBoNZ/RtKSUlB7969UatWLXTv3h1nzpzB2LFj0axZM/j4+KBChQpm+fuNtMdy/3zI5dLzeZ1OhwcPHgAAQkND0alTJ8yaNQtr166FXq83ZYjZlv5LLzU1FU5OTujduzeCgoKwa9cuzJ49GwCUyp/WPRlj2bJlUbt2bdSqVQvbt2/H2bNnlS+snPjr93U4d+4cli9fjiVLlqBUqVJm8Zk8j06nw8aNG9GtWzcUKFAABoN53gJKRDBp0iR07doVffr0wbfffoty5cph3rx5OHLkCB4/fmzqEF9KfHw8zp49i/379+Pu3buoWbMmatasiStXrpjdZ3Xv3j1cv34dBoMBffv2xc2bN7F161Y0aNAAu3fvZtJHqmDiZ4Ge/Kvw559/xpAhQ9CxY0dcuHABvXr1Qrdu3TB37lwsW7YM8fHxJo42a9Kvadu2bfjss88wb9483Lp1C6GhoahcuTL27t2LadOmAYDmfzk++fn89ttvWLx4MX7++Wf07dsXN27cwK+//oq4uDhs3boVs2fPNqsvL4PBgISEBIwbNw4bNmzAsWPHAKR1z5tr8hcdHY0vv/wSv/76K2rUqJGhu81crislJQWxsbGIiYnB7du30aJFC8THxyMqKgp9+vTB1q1bTR3iS2nSpInyB9LatWtx7NgxxMTEwMvLy6y6RvPkyYOPPvoIU6dORaNGjVC0aFFs2rQJM2bMQGhoKEaNGoWUlBRTh0kWwHz+VVCWpScVkyZNwp9//olvv/0WERER6NWrF3bv3o1u3bqhdevWWLlypVl8aRkMBqXq8vHHHyMwMBDjx4/Hl19+iUOHDiE0NBS+vr7Yu3cvrl69aupwXyj985kyZQqmTJmCunXrQqfToXbt2ujRowfu3buHli1bYsCAAahfv75ZfHk9+XNkb2+PMWPGoHnz5jh16hT27NkDwHyqsU8zGAyoWLEiqlWrhqSkJCQnp80SvHDhgvKzqWX//fcf7t27h7x582LgwIGYN28eevTogUKFCmHdunWYMmUK2rZti7FjxyrXplX//fcfDh8+jI0bNyI6OhoAkJycjMTERPz2229o1aoVvvnmG+h0OjRo0MDE0b7Yk9dz+vRp1K9fH2fPnsX777+PmjVrKsf5+/ujZMmSZvG7gMyAkMW4fPmy3Lt3T0REzpw5I61bt5b79+/LTz/9JC1atJDevXuLr6+vbNu2TURE7t+/b8pwXyj9WkREoqOjJTAwUM6dOydbt24VX19f+fDDD6V169Zy4MABSU5OlkuXLpkw2uzZs2eP+Pv7y7179+Tx48eyYsUKGThwoNy9e1fu3LkjGzdulPPnz5s6zCwxGAwiIrJ582bp27evfPvtt7J582a5c+eO9O7dWwYPHizbt283cZRZo9frM+w7fvy4FClSRHbu3Knsi4yMlC5dusjNmzdfZ3jZdvLkSalXr56sX79ekpKSRETkwYMHsnjxYhk/frxy3NatW6Vjx47y+PFjU4X6QqdPn5a33npLWrVqJW+99ZbY2trK1KlTJSkpSb777jtxcXGRH374QTk+IiLChNG+2LOuZ/r06SIicuDAAWnYsKEsWLBADhw4IAEBAbJu3ToTR0yWgomfhbh27Zp8+umnEhcXJ8nJyZKcnCzXrl2TTZs2SbVq1UREJDU1Vdzc3KRXr16SkJBg4ogzFxcXJ0OHDpWrV68q+86cOSORkZESFBQk8fHxcuLECXFxcZEuXbrI3bt3TRjti6UnR+lOnjwpffr0kdDQUBk0aJB06dJFmjZtKu3atZPU1FQTRfnyNm7cKAEBAbJ27Vpp2LChNG/eXERELl68KJ07d5YPP/zQKJHXohMnToirq6vMmDFD2Zf+uU2ZMkXKlCkjkydPloULF4qfn5+sXr3aVKFmycWLF8XX11d+/fXXDM8dOHBAKlSoIL/88ots375dAgICNH09Z8+elcqVK8vs2bOVfQsWLBBnZ2eZM2eOnDhxQv78808REUlJSVGOefrfnVY863p+//13cXZ2lilTpkhKSopMnDhRAgICpFGjRrJq1SoR0e71kHlh4mcB4uPjRUTk0aNHsn//fhk/frz8999/IiLyxx9/yCeffCIiIsuWLZMPP/xQLl++bLJYsyIxMVFERGJiYuTixYvy448/Ks/NmzdP+vTpIyJpyVPr1q3lzJkzJokzq578ZZ2ecMfHx8v48eOlV69ecuzYMRERWbFihfTr18+sEj+DwSD37t2Tjh07yoULF2THjh1SpUoVuXLlioiIPHz4UK5duyYnTpwwcaSZu3XrltSqVUveffdd8fHxkTlz5hg9n5ycLEuWLJGWLVtKv379ZOPGjSKi7S/iZcuWSWhoqIiIJCUlyS+//CJjx46V/fv3i4jI/PnzJSAgQFq0aKEkfVq8nitXroiXl5f8/PPPIpL2B2x6nAsWLJA8efIY/Q7Q+r+fzK5n4cKFYm1tLadPnxaRtN8T6X8wafGzIfNkvnP4CQBw8uRJfPfddxgyZAhCQkJw/Phx7N69G/b29mjfvj2KFCmCP/74A3fv3kVERAQ2btyIEiVKmDrs5/rvv/8wduxYvPfee6hevTp+//13bNy4EdbW1hg0aBCKFi2KS5cuYcCAAdi9ezfGjx8PHx8fU4f9XPLERI5p06Zh586dcHNzQ8OGDTF06FDluAULFuDnn3/GnDlzzGocj06nQ6FChVCuXDn89NNPOHjwIJYsWQIPDw9s3LgRJ0+exJAhQ/DGG2+YOtRMWVlZoX379vjwww+xatUqfPzxxwCArl27wsrKCnny5EG7du3QqlUr5M2bV3mdlsf3PX78GKmpaberatOmDQoXLoz8+fPj3XffxcKFC9GlSxe0atUKOp0O+fPn1+xSIdHR0fD29oaI4M6dO3B2dkZKSgqsra3RqVMnzJkzBwcOHICPj49ytwsty+x6OnbsiF9//RUHDx5EuXLlUKBAAeV1WvxsyDwx8TNz6b9Apk6dinz58qF3796ws7PDmjVrICLo168fVqxYgZiYGHz++ecoU6aMqUPO1I0bNxAfH48lS5Ygf/786NSpE6ytrbFs2TLY2toiNDQUCQkJOHXqFH744QfNDuBO/xJN/++0adOwYsUK/PDDDxg2bBg2b96MixcvYsCAAdiwYQPmzp2LuXPnmsViuulrDKakpCBPnjxISkrC7du3sW7dOqxcuRJeXl44cOAAhgwZgokTJ2p+LTWDwQBnZ2d0794dANCyZUsAwMcffwyDwYCePXvi0KFD8PLygrOzswkjzZ6qVati5MiR6NWrFwICAhAWFgYA8PT0xPfff49atWppOrG4efMmbty4gfr16+Phw4dYu3YtJk+ejI8++gjOzs5ITk5G3rx5UaxYMbi6ugLQ3jU86WWuhyhHmKzWSK/EYDAoY1keP34sXbp0kbZt28rff/8tIiK//fabtG3bVsaPH6/5sVVPO3z4sHz88cfSt29fOXbsmBgMBlmwYIG89957MmnSJKNjtdr9cerUKeX/jx49Ks2aNRO9Xi8//fSTNGrUSObNmyfly5dXxl+ld81rWXx8vCQnJ4uIyF9//SXt2rWTSZMmyb///it3796V2rVrS5s2baR9+/bi7+8va9euNXHEmXvw4IGIpP0MPflzlN5VuGLFCvHx8ZEhQ4aIg4ODbNmyxSRxZtWtW7dk//79smbNGmX4wKJFi6RixYrSrVs35bj169dLz549NftvRyRtPG9gYKB89dVXStf0kiVLpGfPnvLNN99ITEyMiIjs2rVLAgMDjf69aZGlXQ+ZNyZ+ZujJX9ixsbEikjYGqW/fvkbJ36xZs6Rbt25mkfg9/SX0zz//yMCBA6VPnz5K8jdnzhxp1qyZ/PvvvyaKMmsSEhLEx8dHunTpouy7du2a7NixQ95++20REbl+/bq89dZbUrNmTc3PrhZJS0w/+ugjWb16tezYsUP8/Pzku+++kxYtWsiHH34op06dEr1eL5s2bZLVq1criYdWk4sTJ05IoUKF5JdffsnwnMFgUJK/r7/+WnQ6naxZs+Z1h5gtp0+flipVqkjr1q2levXqkjdvXpk6dapcuHBBJk2aJMWLF5fw8HBZt26dVKxYUdNJ+enTp6V8+fIyd+7cDM8tXbpUevbsKRMnTpS1a9dKYGCgpieliFje9ZD5Y+JnZp78Ip0+fbp07NhRPvnkE/njjz8kJSVFunbtKp06dZIjR46ISNrsWK17cjmQTz75RL766is5ffq0XLx4UT7++GPp37+/HD58WAwGg1y/ft3E0WbN2bNnpWLFitKrVy9l38KFC6V+/foiIrJ27Vrp37+/3Llzx1QhZsvDhw+lf//+0qdPH+nbt69s2rRJRET+/vtv5TPatWuXiaPMmps3b0pgYKC0b99e8uXL98zkTyTtMwwICFBmiz5dGdSKM2fOSEhIiNEM0WXLlknRokVl8uTJIiJy8OBB6dKli3z88cdK0qfFa0lOTpYPPvhAmfjw5P50K1eulJYtW0rhwoWVJU60eC0ilnc9ZBmY+JmpmTNnSt26deXkyZNSqVIl6devn4ikzd5r3bq19OjRQ9Nrcj1t3bp1EhQUJGvWrJGaNWsqy4EcPXpUQkNDpVevXs9cY01Lnv5lfe7cOfHx8ZHevXuLSNpsvurVq0udOnWkXLlycvLkSVOEmS1PXtP9+/flq6++koCAAPnyyy+V2dcnTpyQvn37SmhoqFkksvfv31fWsNu4caNYWVkpyd+TS4HcuHFDjh49KiLaTfquXr0qXl5eMmHCBBFJ66Z+sqvayspKDhw4ICLGn6UWryVd06ZNlZifTJBEROnNWLdundJlquVrEbG86yHzx8kdZkZEcO/ePezduxeLFy/G2rVrUbRoUfz8888wGAx48OABFi1ahDt37sDW1tbU4WbJnTt38Pvvv2P9+vU4fPgwDAYDJk+eDADw8/PDRx99BABwcHAwZZgvJP+byHHv3j2kpKSgTJkyWLNmDVq1aoUPP/wQ06dPx/Lly7Fr1y5UrlwZXl5epg45U48ePcLSpUvRrVs3bNmyBZGRkfj000+h0+lw7do1/PXXX3jnnXdQoUIF9O/fH3ny5EGRIkVMHfYLOTk5oX///jAYDGjcuDHWr1+Ppk2bwmAwoE+fPjh69CiKFCmCkiVLws3NDYB2Jw2cP38ePj4+MBgMiImJgaurK1JTU2EwGNCqVSs0aNAAJ06cQEhIiNHrtHo9AJCQkICIiAiEhIQgT548SE1NhU6ng5WVFTZu3IgHDx6gadOmyvFavhbA8q6HzJ+2570TAOPbYel0OhQoUAAuLi745JNPsHr1aqxduxZ58uTBtGnTMHPmTOTJk0f5wjIHTk5O8PDwwJgxY/Dtt9/i999/R8mSJbF69WpMnz4d5cqVQ/ny5U0d5nNFRUUBSFsSZNy4cXj33XdRs2ZNjBgxAkWKFMHKlSuxZ88edO7cGa6urmjbtq3mkz4AsLOzw+nTp1GoUCEMGzYMDRo0gIODAz777DMUK1YMa9euxcqVK5GSkgI/Pz+UK1fO1CE/1+3bt3HgwAGsXbsWx48fR758+WBlZYXU1FQ0btwYa9euxaBBgzB48GC89957uHTpkqlDztSNGzewf/9+1KlTBwMGDMDZs2cxZcoUxMTEwNraWrm/s7Ozs1nMeAX+//dco0aNcPToUezevRsAYG1tDSsrK+zbtw+LFi2Cvb29KcPMMku7HrIgJq03UrY8OV6vf//+4uDgoHStLVmyRCpWrGgWs8Ge7E4TSev+GDx4sFSoUEH27t0rIiK7d++WsmXLytatW00RYpYlJiZK5cqVpXnz5nLw4EFp1aqVXLhwQfbv3y9169aV4cOHi0jaOKy33npLbty4YRZdOekxXrlyRdzc3OSNN94wev7hw4cyePBg6dy5szIjUaueNfHh559/locPH4qIKLcyGz16tOh0Os0Prj9z5oxUqlRJvv76a9m3b5+IpHXr9urVS7788ku5du2aiKTNEA0ICND84tlPu3LlirRo0UI6duwo06dPl3/++Uc2bNggZcuWNcvbllna9ZD5Y+KnYf/9958yTm/KlCnSoEEDGTRokFy8eFFERJo0aSK1a9eWFi1aSEhIiOZ/waffYUQkbTmQ0NBQ+fXXX+XKlSty5swZadKkibz//vvSu3dvKV++vNn8Urxx44a8/fbb4uXlZTRz7+zZs1KkSBHZvHmziPx/gqF1T99p5O7du9K5c2fx9PRUZojfvHlTbt26JdHR0aYKM0ueNfFh+fLlUrRoUWVcnEjaWCtvb29ZuXKliGh3nNWZM2ekXLlyz5whmp78jR8/XlauXClBQUGaT2Kflj4+8fLly/LVV19JzZo15a233jK6u4g5sbTrIcvAxE+j/vnnH2ncuLFs2bJFlixZIsHBwbJp0yZp0qSJ9O7dW44fPy4iIocOHZLDhw9rfrbrnTt3ZODAgbJ27VqJjIyUgIAAGTx4sHTq1Em6du0qly9flps3b8qaNWtk8eLFcujQIVOHnKmnE4ObN29K5cqVJSgoyGh/nz59ZPv27a8ztFeSfl1bt26VMWPGyGeffaasMdipUycpVaqU/PHHH+Ls7Kz5PzQym/jw559/ipWVlfJzdu/ePaPB9VpM/FJSUqRbt27y008/Ge1/csLAunXrpFWrVlKkSBFNzxBN/4P2WbdXS9+X/t/4+HilOqvFaxGxvOshy8YxfhpVunRplC9fHj/99BP27duH8ePHo2HDhpgzZw4MBgOmTJmC3bt3Izg4GEFBQXB3dzd1yJlKSEiAjY0NNmzYgAkTJuCXX37BTz/9hIEDB6JYsWIICwtDTEwMmjdvjvbt2yM4ONjUIT+XPHFrq7t37+LmzZtwdXXF2rVrkZqaiiZNmuDkyZNYsWIFNm3ahJIlS5o44qzT6XTYvHkzPvroI7i6uuLAgQMYNGgQdu7ciQULFuDdd9/F2rVrMW/ePM3fZeTpiQ9WVlYQERgMBrz33nto0KABjh8/DgAoVKgQqlSpAiDtPdDieDhra2v8999/yr+NlJQUAECePGlz9I4dO4amTZuib9++2LBhA5o2baq527CJCO7evYvSpUvjwIEDsLKyUsYjpku/5Vp63AUKFEC+fPmM9mmFpV0P5Q5M/DQmKSlJ+f8ff/wRDRs2xKZNm7B//348ePAArq6uGDNmDO7fv4+VK1fi0aNHJoz2xdK/aD08PNC7d28UK1YMJ06cwPbt2wEAISEhaNu2LRwcHDBu3DjExcVl+MWpNem/rCdPnow2bdrg7bffxoABAyAi2LhxI/777z+89dZbuHDhArZu3YpSpUqZOOKsS0xMxJQpUxAWFobu3btj27Ztyi2+AGDChAmYPn26klRoUXYmPri4uJg42uxJTU1FREQEACgzRNOvZ+vWrYiIiEDDhg2VWbxaSyx0Oh2KFCmCtm3bKpMenpUspR+bfq/h9ORJaz9zlnY9lDsw8dMQvV6Pn3/+GTdv3sTs2bPRt29fDBo0CB06dMDu3buxb98+PHz4EMWKFcPUqVPxySefwM7OztRhP9fDhw+xadMmWFlZYf369YiIiEC3bt3Qrl07nD17FitXrgQABAcH44MPPsCIESPg5OSk+ZusA8Dvv/+OefPmYfbs2fjzzz/x77//4ttvv1Uqfw0aNECbNm1QunRpU4eaLXny5IGNjQ0KFy4MIO0LatSoUbh37x7Onz8PAMifPz8A7SUVAHD27Fk0a9YMGzZsQGRkJN555x00adIEt27dwpQpU3D9+nXkyZMHu3fvxqlTp8ymGpueINSvXx9///03duzYAcB4hujvv/+u6SWPrl+/jubNmwNI+6N24MCBqFGjxnOTpfR7QsfFxSE8PBwGg0FTP3OWdj2Ui5ioi5meY9asWWJjYyOBgYFGkyG+/PJLadGihaxdu1YZH6J1iYmJ0qVLF6latar4+PjIjh07RETk4sWLEhYWJr169ZIlS5aYNsgsenIsTmJiokyaNEnmzZun7Lt165a4ubnJsmXLRCTjzGVz8tlnn4mHh4eyYPaxY8ckMDBQ8+NILX3ig0jaWNJWrVpJu3bt5Oeff5ZTp07JunXrxNvbW/OTofR6vbz11ltSr149Zd/XX38tdnZ2yp2G0sfBpf/7iY2Nlbp168ru3btff8AvYGnXQ7kHEz+N2bt3r3h4eIiLi4tcunTJ6LnPP/9c3nvvPeXm8lp1+vRpWbBggYiIHDhwQIoWLarcqiz9F+CVK1dk+PDh0q1bN7l586bJYs2KJ5O+OXPmyO+//y5DhgyRkJAQo+MGDx6szOA1R09eZ48ePcTDw0MGDRokfn5+mk+SLGniw/OkJxE3b96UsLAwqVevnlSpUkXee+895V7CWr8evV4vjRo1kpo1ayr7RowYIY6OjsrkmvTP7P79+1KvXj1N3wrQ0q6Hcgcmfib2vF/UEyZMEHd3d+Uvx+3bt0tqaqqynIaWHTt2TLZt2yZ37tyRx48fy7Fjx6Ru3brSoUMHuXXrloikrUl45MgROXfunImjzbotW7ZIhw4dlM+gRYsW8t5778n169dl7ty5UqFCBblw4YKJo3w1T/48rl69WrZt22Y2t5Jq2rSpUkl5+tZY6bde27Rp0zNvYaYlT1b6n5ae/KXH/uDBA3n06JHRPq15Oq6HDx9KkyZNpEaNGsq+zz77TPLkyaOsVXrv3j2pU6eOJitjlnY9lPsw8TOhJ3+BzJgxQz766CNp0aKFnDhxQpKTk2X06NHi4OAg33zzjbi4uCjr92lZ+hfTgwcPRKfTyffffy8iaV9mNWrUkM6dO8tvv/0mpUuXln/++ceUoWbLsWPHpHjx4jJy5EgRSevuvXz5srRs2VKaNWsmderUMYt77z7L019kz1qSIrP9WtG4cWP57rvvlMcpKSlKzD/88IPs3LnTVKFl2f379yUwMFCpmD/Pk5+Zlj+X9Dg3b94sP/zwg4wePVpiY2Pl/v370qRJE6lbt65y7JN/BM6aNUu2bdv22uN9EUu7HsqdmPhpwJQpUyQkJES2b98uPXr0kNq1a0tERISIiPzyyy/Sr18/OXPmjImjzL6tW7eKvb29/PDDDyKSlgx2795dOnbsqCyUq1VPf5neunVLQkNDxc3NLcP6dY8ePTKbcZfpX1x3795V1h4TeXbykN4tr9VKUrr0+MaPHy9t2rTJsG7i3r17xc/PT6mea93cuXOlfPnyL+xeN5dxpOvXr5eKFSvKrl27xN7eXj755BMRSesmrVmzplStWlVEMlZptcrSrodyHyZ+JvBkd21SUpK0atXK6Evpm2++ER8fH0lMTBQR8/gFkv7le+TIEVmzZo0cPHhQREQOHjwoefPmlfHjx4tI2pdVeleW1hMKkbQ7OkRHR0tSUpKkpqbK8OHD5a233pK///5bRMzjGp62du1a8fX1la5du0p4eLiy/8nkLz2puH//vkybNk0SEhJee5zZZc4TH0SMx782bNhQnJycZOnSpZkee//+fRk5cqRmf0fExsZKkyZN5PLly7J582YJCQlRbiknkvbHYHrXezot/5uytOuh3ImJ32tkMBjkwoULUqZMGWUSwMOHD6VSpUqyaNEio2ObN2+u+UkPT9u6dau88cYb0rVrVylRooR89913kpqaKvv27ROdTifjxo0zdYgvFBkZqXQJTpkyRcqWLSuNGzeWcuXKyZkzZ+TBgwfy1VdfiY+Pj1ncF/lpFy5ckHfeeUeWLVsmK1askHr16skXX3yhPJ+ammqUVFSrVk327NljqnCzzBImPoiIrFmzRipXriwrV66UQYMGSZEiRTIkf1qeIfr0exwfHy9du3aVyZMnS40aNeTs2bMiIjJ79myj2+hplaVdD5EIEz+T+P777yUoKEhJMGbPni2+vr7K4wULFkhgYKDmJ3LcuHFDYmJiRCTt1nHffvut7N27V0TSJkK8//778uuvv4qIyM6dO2XDhg0mizWrwsPDxcfHRyZOnChdunRRxlUOHTpUypQpI/fu3ZMHDx7I6NGjzWLMpcj/f3mdO3dOduzYoXS9x8fHy86dO6V+/foydOhQo9ekz0DUUlIhYnkTH56+nt69e8vChQuVx3PmzBFHR0dZvHixiBhX+rQ8Q/Tw4cPK8JQuXbpI4cKF5fz58yIisn//filfvrxs2rTJlCFmi6VdD+VuTPxekyfv/3ngwAGpUqWKFC1aVLZs2SLJycny888/S8GCBaVr167i7++v+YkCZ86cEW9vb9m6das8fvxYChYsKK6urnL+/HnlC3j27NkSFBRk9OWm1S/gM2fOyKNHjyQ2NlZ+/vlnKV++vPTv319E/j/md999V8aOHSsi5jO+Kt1ff/0lhQoVkoYNG4qdnZ1y/92EhATZunWrUfUiLi5OqlSpormkz5ImPhgMBomLi5OgoCDls0hOTpZ3331XPvvsMxH5/9jr1KkjpUqVktu3b4tI2lCR2rVra+rzOX/+vDK2cuPGjfLmm29KZGSkpKamysGDB6VTp05SpUoVGT16tPj5+cnatWtNHHHmLO16iJ7ExO81Gz9+vLz99tuyZcsW6dGjh7zxxhvKbK/jx4/L6dOnjcaMaNHZs2flrbfeMurauHr1qri7uysDnUXSBtU3b95c8+PDVq9eLVWrVpXY2FhJSUkRg8EgI0aMkEKFChl1cw4dOlR+/PFHE0b6cqKiouS7776T3bt3S0pKivTq1UvKli2rDCVISEiQu3fvikhaQrJ8+XLZt2+fKUN+Lkub+BATEyPHjx9XFv7es2ePlC9fXn755RcRSRt6MHToUDl8+LDymnnz5mlqhujZs2clODhY/vzzTzl//rz4+voaVSINBoPcvXtXfvrpJ1myZImmEtZnsbTrIXoaE7/XKCkpSVq2bGn0S/ybb74RZ2dn2bhxowkjy7ro6GipUKGCcneElJQUCQkJkYMHD8rZs2elQIECUr9+ffnll1+kZs2a8scff5g24BfYtGmTBAQEyK5du+T06dPSqVMnpYt99OjR4u3tLZMnT5bVq1dLYGCgWY3rS01Nlfv370uePHkkJCREEhISxGAwSEpKivTr10/c3d2fOY40KSnJBNFmztImPjxZlZw/f77odDpZsWKFiKTdZcTFxUVat24tHh4emh4icfbsWfH395f169eLiEhERIR0795dRNJ+/tJnjt+5c8dkMWaHpV0P0bMw8ctBT3drPnjwQAIDA2Xq1KnKPr1eL76+vlK7dm3NV8ZE0sby5c+fX27cuCEiad2fH3/8sfL85cuXxdnZWerXr690TWm1+rJx40YJDAxUbiW3atUq6du3r3z44Ydy//59EUlLzHU6nfTo0SPD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", "text/plain": [ "
" ] @@ -907,10 +914,10 @@ "id": "1671c31a24314836a5b85d7ef7fbf015", "metadata": { "execution": { - "iopub.execute_input": "2026-08-17T07:37:49.314476Z", - "iopub.status.busy": "2026-08-17T07:37:49.314375Z", - "iopub.status.idle": "2026-08-17T07:37:49.553032Z", - "shell.execute_reply": "2026-08-17T07:37:49.552436Z" + "iopub.execute_input": "2026-10-13T18:07:06.709025Z", + "iopub.status.busy": "2026-10-13T18:07:06.708948Z", + "iopub.status.idle": "2026-10-13T18:07:06.949882Z", + "shell.execute_reply": "2026-10-13T18:07:06.949267Z" } }, "outputs": [ @@ -948,47 +955,36 @@ " \n", " \n", " -1\n", - " 0.12\n", - " 0.54\n", - " 0.18\n", - " 0.30\n", - " 0.16\n", - " 0.02\n", - " 0.12\n", - " 0.30\n", - " \n", - " \n", - " 1\n", - " NaN\n", - " 0.20\n", - " 0.13\n", - " 0.13\n", - " -0.04\n", - " -0.05\n", - " -0.07\n", + " 0.08\n", + " 0.64\n", " 0.23\n", + " 0.24\n", + " 0.08\n", + " 0.08\n", + " 0.10\n", + " 0.28\n", " \n", " \n", - " 2\n", - " 0.16\n", - " 0.35\n", - " 0.17\n", - " 0.14\n", - " 0.07\n", - " 0.02\n", + " 1\n", + " 0.11\n", + " 0.77\n", + " 0.27\n", + " 0.15\n", " 0.04\n", - " 0.18\n", + " 0.09\n", + " 0.06\n", + " 0.25\n", " \n", " \n", - " 3\n", + " 2\n", " 0.08\n", - " 0.64\n", - " 0.17\n", - " 0.09\n", + " 0.82\n", " 0.09\n", - " 0.11\n", - " 0.05\n", - " 0.25\n", + " 0.18\n", + " 0.12\n", + " 0.02\n", + " 0.21\n", + " 0.38\n", " \n", " \n", "\n", @@ -996,16 +992,14 @@ ], "text/plain": [ " Endothelial Epithelial Fibroblast Imm_other Myeloid_CD11c \\\n", - "-1 0.12 0.54 0.18 0.30 0.16 \n", - " 1 NaN 0.20 0.13 0.13 -0.04 \n", - " 2 0.16 0.35 0.17 0.14 0.07 \n", - " 3 0.08 0.64 0.17 0.09 0.09 \n", + "-1 0.08 0.64 0.23 0.24 0.08 \n", + " 1 0.11 0.77 0.27 0.15 0.04 \n", + " 2 0.08 0.82 0.09 0.18 0.12 \n", "\n", " Myeloid_CD68 Tcell_CD4 Tcell_CD8 \n", - "-1 0.02 0.12 0.30 \n", - " 1 -0.05 -0.07 0.23 \n", - " 2 0.02 0.04 0.18 \n", - " 3 0.11 0.05 0.25 " + "-1 0.08 0.10 0.28 \n", + " 1 0.09 0.06 0.25 \n", + " 2 0.02 0.21 0.38 " ] }, "execution_count": 12, @@ -1067,7 +1061,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.2" + "version": "3.11.11" } }, "nbformat": 4, diff --git a/vignettes/03_nested_cv_niches_and_metaclusters.ipynb b/vignettes/03_nested_cv_niches_and_metaclusters.ipynb new file mode 100644 index 0000000..f079102 --- /dev/null +++ b/vignettes/03_nested_cv_niches_and_metaclusters.ipynb @@ -0,0 +1,1363 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4fd00e50", + "metadata": {}, + "source": [ + "# 3 - Nested cross-validation, niches and cross-fold metaclusters\n", + "\n", + "Vignette 2 pretrained an encoder and fine-tuned one model on one split. This\n", + "notebook runs the preprint's **analysis chain** on the same tiles:\n", + "\n", + "1. `grass-mil-nested-cv`: outer folds, inner epoch selection, refit, out-of-fold scores;\n", + "2. `grass-mil-extract`: every cell of every tile through every fold model;\n", + "3. `grass-mil-niches`: niches of one fold model with attention-weighted attribution;\n", + "4. `grass-mil-metaclusters`: which niches replicate across the fold models;\n", + "5. `grass-mil-scores`: bootstrap intervals and operating points of the out-of-fold scores;\n", + "6. `grass-mil-spatial-stats`: the model-free comparison.\n", + "\n", + "---\n", + "\n", + "### Read this before interpreting any number below\n", + "\n", + "The dataset is three images from two donors, cut into ~40 tiles. The\n", + "**unit** of everything below is a tile (`id_kind=patch`): tiles are the bags,\n", + "the labelled units and the fold members. The label is the synthetic\n", + "epithelial-rich flag of vignette 2, a positive control. Tiles of one image are\n", + "grouped as one \"patient\" so that no image contributes to both training and\n", + "test of a fold, which leaves three outer folds (one image each) and one image per inner\n", + "fold. Everything runs in a few minutes on CPU with two epochs\n", + "per run, so the numbers show the mechanics and the outputs, not a result.\n", + "The preprint uses 5 outer folds, 3 inner folds, 25 epochs and whole cores as\n", + "units.\n" + ] + }, + { + "cell_type": "markdown", + "id": "09969f8e", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "06ba85eb", + "metadata": { + "execution": { + "iopub.execute_input": "2026-10-13T18:19:27.224086Z", + "iopub.status.busy": "2026-10-13T18:19:27.223978Z", + "iopub.status.idle": "2026-10-13T18:19:27.409690Z", + "shell.execute_reply": "2026-10-13T18:19:27.409285Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "pretrained encoder: logs/pretrain_a3/runs/2026-10-13_20-06-18/checkpoints/last.ckpt\n" + ] + } + ], + "source": [ + "import json\n", + "import os\n", + "import pathlib\n", + "import subprocess\n", + "import sys\n", + "import glob\n", + "\n", + "import pandas as pd\n", + "\n", + "WORK = pathlib.Path(\"vignette_work\").resolve()\n", + "RAW, PROCESSED = WORK / \"data\" / \"raw\", WORK / \"data\" / \"processed\"\n", + "assert (RAW / \"patch_labels.csv\").exists(), \"Run vignettes 1 and 2 first.\"\n", + "env = dict(os.environ, PROJECT_ROOT=str(WORK))\n", + "\n", + "DATA_ARGS = [\n", + " \"data=spatial_omics\",\n", + " f\"data.raw_manifest_path={RAW}/manifest.csv\",\n", + " f\"data.processed_dir={PROCESSED}\",\n", + " \"data.h5ad.coord_source=obsm\",\n", + " \"data.h5ad.coord_key=spatial\",\n", + " \"data.h5ad.categorical_label_columns=[cell_type]\",\n", + " \"data.coord_scale_um=0.39\",\n", + " \"data.sample_unit=tile\",\n", + " \"data.tiling.tile_size_um=100\",\n", + " \"data.tiling.stride_um=100\",\n", + " \"data.min_cells=20\",\n", + " \"data.tiling.min_cells=20\",\n", + " \"data.graph_builder.kwargs.edge_features=[log_distance,neighbor]\",\n", + " \"data.graph_builder.kwargs.neighbor_cutoff_um=20\",\n", + " \"data.split.train_val_test_split=[0.34,0.33,0.33]\",\n", + " \"data.split.split_by=sample\",\n", + "]\n", + "EGO_ARGS = [\"data.sampler.runtime.depth=2\", \"data.sampler.runtime.subgraph_batch_size=32\"]\n", + "BAG_ARGS = [\n", + " \"data.sampler.runtime.subsample_count=32\",\n", + " \"data.val_sampler.runtime.subsample_count=64\",\n", + " \"task.region_accumulation.hyperbatch_size=4\",\n", + "]\n", + "bgrl_ckpt = sorted(\n", + " glob.glob(f\"{WORK}/logs/pretrain_a3/runs/*/checkpoints/last.ckpt\"), key=os.path.getmtime\n", + ")[-1]\n", + "print(\"pretrained encoder:\", os.path.relpath(bgrl_ckpt, WORK))\n", + "\n", + "\n", + "def run(args, tag):\n", + " out = subprocess.run([sys.executable, \"-m\"] + args, capture_output=True, text=True, env=env)\n", + " print(f\"{tag}: exit {out.returncode}\")\n", + " if out.returncode:\n", + " print(out.stdout[-3000:], out.stderr[-3000:])\n", + " raise RuntimeError(f\"{tag} failed - see output above\")\n", + " return out" + ] + }, + { + "cell_type": "markdown", + "id": "153d520b", + "metadata": {}, + "source": [ + "## 1. Labels, patients and folds\n", + "\n", + "The label file needs the unit id, the target and a patient column. Here a\n", + "\"patient\" is an image, so tiles of the same image never straddle a fold.\n", + "Outer folds are a JSON list of lists of unit ids.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "4a4c90dd", + "metadata": { + "execution": { + "iopub.execute_input": "2026-10-13T18:19:27.410658Z", + "iopub.status.busy": "2026-10-13T18:19:27.410521Z", + "iopub.status.idle": "2026-10-13T18:19:27.417758Z", + "shell.execute_reply": "2026-10-13T18:19:27.417459Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " tiles positive\n", + "patient \n", + "point16 16 6\n", + "point23 12 5\n", + "point8 16 11\n", + "fold sizes: [16, 12, 16]\n" + ] + } + ], + "source": [ + "index = json.loads((PROCESSED / \"processed_index.json\").read_text())\n", + "entries = index[\"entries\"] if \"entries\" in index else index\n", + "labels = pd.read_csv(RAW / \"patch_labels.csv\")\n", + "image_of = {e[\"patch_id\"]: e[\"sample_id\"] for e in entries}\n", + "labels[\"patient\"] = labels[\"patch_id\"].map(image_of)\n", + "labels = labels.dropna(subset=[\"patient\"])\n", + "labels.to_csv(RAW / \"tile_labels.csv\", index=False)\n", + "print(labels.groupby(\"patient\")[\"epithelial_rich\"].agg(tiles=\"size\", positive=\"sum\").to_string())\n", + "\n", + "# Three outer folds, one image each: every fold model trains on two images\n", + "# (one per inner fold) and is scored on the third.\n", + "images = sorted(labels[\"patient\"].unique())\n", + "folds = [labels.loc[labels.patient == im, \"patch_id\"].tolist() for im in images]\n", + "(RAW / \"folds.json\").write_text(json.dumps(folds))\n", + "print(\"fold sizes:\", [len(f) for f in folds])" + ] + }, + { + "cell_type": "markdown", + "id": "036ccb6b", + "metadata": {}, + "source": [ + "## 2. Nested cross-validation\n", + "\n", + "`regime=mil` is the GRASS-MIL default. For every outer fold the driver splits\n", + "the development tiles into `inner_k` image-grouped inner folds, trains with\n", + "region-level selection, takes the median best epoch, refits on all development\n", + "tiles for that many epochs and scores the held-out tiles once, on all cells.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "9bf8551f", + "metadata": { + "execution": { + "iopub.execute_input": "2026-10-13T18:19:27.418617Z", + "iopub.status.busy": "2026-10-13T18:19:27.418479Z", + "iopub.status.idle": "2026-10-13T18:19:48.677674Z", + "shell.execute_reply": "2026-10-13T18:19:48.677326Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "nested CV: exit 0\n", + "{'auc_mean': 0.6659451723098755, 'auc_sd': 0.28989991652601, 'auc_folds': [0.3667, 0.6857, 0.9455], 'patient_auroc': nan, 'complete': True}\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
outerbag_idregion_idsample_idlogit_0logit_1prob_class_0prob_class_1labelpatient_id
00point16::point16::point16_point16_patch_0point16_point16_patch_0point160.2371920.0552300.5453650.4546350point16
10point16::point16::point16_point16_patch_1point16_point16_patch_1point160.2329360.0598240.5431700.4568300point16
20point16::point16::point16_point16_patch_10point16_point16_patch_10point160.2351870.0609470.5434500.4565501point16
30point16::point16::point16_point16_patch_11point16_point16_patch_11point160.2352130.0621550.5431570.4568431point16
40point16::point16::point16_point16_patch_12point16_point16_patch_12point160.2341230.0592220.5436140.4563860point16
\n", + "
" + ], + "text/plain": [ + " outer bag_id \\\n", + "0 0 point16::point16::point16_point16_patch_0 \n", + "1 0 point16::point16::point16_point16_patch_1 \n", + "2 0 point16::point16::point16_point16_patch_10 \n", + "3 0 point16::point16::point16_point16_patch_11 \n", + "4 0 point16::point16::point16_point16_patch_12 \n", + "\n", + " region_id sample_id logit_0 logit_1 prob_class_0 \\\n", + "0 point16_point16_patch_0 point16 0.237192 0.055230 0.545365 \n", + "1 point16_point16_patch_1 point16 0.232936 0.059824 0.543170 \n", + "2 point16_point16_patch_10 point16 0.235187 0.060947 0.543450 \n", + "3 point16_point16_patch_11 point16 0.235213 0.062155 0.543157 \n", + "4 point16_point16_patch_12 point16 0.234123 0.059222 0.543614 \n", + "\n", + " prob_class_1 label patient_id \n", + "0 0.454635 0 point16 \n", + "1 0.456830 0 point16 \n", + "2 0.456550 1 point16 \n", + "3 0.456843 1 point16 \n", + "4 0.456386 0 point16 " + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "NESTED = WORK / \"nested_mil\"\n", + "run(\n", + " [\n", + " \"grass_mil.nested_cv\",\n", + " \"regime=mil\",\n", + " \"trainer=cpu\",\n", + " \"trainer.precision=32\",\n", + " f\"model.init_from_ckpt={bgrl_ckpt}\",\n", + " *DATA_ARGS,\n", + " *EGO_ARGS,\n", + " *BAG_ARGS,\n", + " f\"data.graph_labels.label_file={RAW}/tile_labels.csv\",\n", + " \"data.graph_labels.id_column=patch_id\",\n", + " \"data.graph_labels.tasks=[epithelial_rich]\",\n", + " \"data.graph_labels.patient_column=patient\",\n", + " \"data.graph_labels.scope=patch\",\n", + " \"data.force_precompute=true\",\n", + " \"nested_cv.id_kind=patch\",\n", + " f\"nested_cv.folds_file={RAW}/folds.json\",\n", + " \"nested_cv.inner_k=2\",\n", + " \"trainer.max_epochs=2\",\n", + " \"trainer.min_epochs=1\",\n", + " f\"paths.output_dir={NESTED}\",\n", + " f\"paths.log_dir={WORK}/logs\",\n", + " ],\n", + " \"nested CV\",\n", + ")\n", + "summary = json.loads((NESTED / \"nested_cv_summary.json\").read_text())\n", + "print({k: summary[k] for k in (\"auc_mean\", \"auc_sd\", \"auc_folds\", \"patient_auroc\", \"complete\")})\n", + "oof = pd.read_csv(NESTED / \"oof_predictions.csv\")\n", + "oof.head()" + ] + }, + { + "cell_type": "markdown", + "id": "4151f8fb", + "metadata": {}, + "source": [ + "`oof_predictions.csv` has one held-out score per tile, `oof_patient_predictions.csv`\n", + "the mean per image. `outer_/result.json` records the inner best epochs and the\n", + "refit length `E`.\n" + ] + }, + { + "cell_type": "markdown", + "id": "c4f6d8c6", + "metadata": {}, + "source": [ + "## 3. Evaluation statistics\n", + "\n", + "`grass-mil-scores` standardises the scores within fold (the three refit models\n", + "have different scales), reports the pooled AUROC with a bootstrap interval, the\n", + "per-fold mean, and operating points at pre-specified \"flag the top q\" rules. At\n", + "patient level there are only three images here, so we score the tiles\n", + "(`patient_level=false`).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "dd0b5844", + "metadata": { + "execution": { + "iopub.execute_input": "2026-10-13T18:19:48.679055Z", + "iopub.status.busy": "2026-10-13T18:19:48.678966Z", + "iopub.status.idle": "2026-10-13T18:19:51.112619Z", + "shell.execute_reply": "2026-10-13T18:19:51.111831Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "scores: exit 0\n", + " 0\n", + "score prob_class_1\n", + "auroc 0.665289\n", + "lower 0.494135\n", + "upper 0.823033\n", + "raw_pooled_auroc 0.402893\n", + "perfold_fold_0 0.366667\n", + "perfold_fold_1 0.685714\n", + "perfold_fold_2 0.945455\n", + "perfold_mean 0.665945\n", + "perfold_sd 0.2899\n", + "perfold_n_folds 3\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ruleflaggedsensitivityspecificityppvnpv
0top 20 % within fold0.250.270.770.550.52
1top 30 % within fold0.320.360.730.570.53
2top 40 % within fold0.430.550.680.630.60
3top 50 % within fold0.500.640.640.640.64
4top 60 % within fold0.610.730.500.590.65
5Youden (all data, optimistic)0.660.820.500.620.73
6rule-out: sens ≥ 90 % (optimistic)0.840.950.270.570.86
7rule-in: spec ≥ 90 % (optimistic)0.180.270.910.750.56
\n", + "
" + ], + "text/plain": [ + " rule flagged sensitivity specificity \\\n", + "0 top 20 % within fold 0.25 0.27 0.77 \n", + "1 top 30 % within fold 0.32 0.36 0.73 \n", + "2 top 40 % within fold 0.43 0.55 0.68 \n", + "3 top 50 % within fold 0.50 0.64 0.64 \n", + "4 top 60 % within fold 0.61 0.73 0.50 \n", + "5 Youden (all data, optimistic) 0.66 0.82 0.50 \n", + "6 rule-out: sens ≥ 90 % (optimistic) 0.84 0.95 0.27 \n", + "7 rule-in: spec ≥ 90 % (optimistic) 0.18 0.27 0.91 \n", + "\n", + " ppv npv \n", + "0 0.55 0.52 \n", + "1 0.57 0.53 \n", + "2 0.63 0.60 \n", + "3 0.64 0.64 \n", + "4 0.59 0.65 \n", + "5 0.62 0.73 \n", + "6 0.57 0.86 \n", + "7 0.75 0.56 " + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "run(\n", + " [\n", + " \"grass_mil.evaluation.cli\",\n", + " f\"input={NESTED}/oof_predictions.csv\",\n", + " f\"output_dir={WORK}/scores\",\n", + " \"patient_level=false\",\n", + " \"n_boot=500\",\n", + " f\"hydra.run.dir={WORK}/scores/hydra\",\n", + " ],\n", + " \"scores\",\n", + ")\n", + "print(pd.read_csv(f\"{WORK}/scores/scores_summary.csv\").T)\n", + "ops = pd.read_csv(f\"{WORK}/scores/operating_points.csv\")\n", + "ops[[\"rule\", \"flagged\", \"sensitivity\", \"specificity\", \"ppv\", \"npv\"]].round(2)" + ] + }, + { + "cell_type": "markdown", + "id": "41c6d560", + "metadata": {}, + "source": [ + "## 4. Extract every cell through every fold model\n", + "\n", + "The cache holds, per cell, the ego-graph embedding, the per-class instance\n", + "logits and the raw attention logits of each refit model, plus the cell types,\n", + "positions and the cell graph. The held-out tiles of every model are recorded.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "d6e04913", + "metadata": { + "execution": { + "iopub.execute_input": "2026-10-13T18:19:51.114276Z", + "iopub.status.busy": "2026-10-13T18:19:51.114113Z", + "iopub.status.idle": "2026-10-13T18:19:56.168329Z", + "shell.execute_reply": "2026-10-13T18:19:56.167918Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "extract: exit 0\n", + "fold0 : 44 tiles, 16 held out, unit = patch\n", + "fold1 : 44 tiles, 12 held out, unit = patch\n", + "fold2 : 44 tiles, 16 held out, unit = patch\n" + ] + } + ], + "source": [ + "run(\n", + " [\n", + " \"grass_mil.extract\",\n", + " \"regime=mil\",\n", + " \"trainer=cpu\",\n", + " *DATA_ARGS,\n", + " *EGO_ARGS,\n", + " f\"data.graph_labels.label_file={RAW}/tile_labels.csv\",\n", + " \"data.graph_labels.id_column=patch_id\",\n", + " \"data.graph_labels.tasks=[epithelial_rich]\",\n", + " \"data.graph_labels.patient_column=patient\",\n", + " \"data.graph_labels.scope=patch\",\n", + " \"task.bag_key=patch_id\",\n", + " f\"extract.nested_cv_summary={NESTED}/nested_cv_summary.json\",\n", + " \"extract.device=cpu\",\n", + " f\"extract.output_dir={WORK}/cells\",\n", + " f\"paths.output_dir={WORK}/extract\",\n", + " ],\n", + " \"extract\",\n", + ")\n", + "for d in sorted(glob.glob(f\"{WORK}/cells/*/index.json\")):\n", + " idx = json.loads(open(d).read())\n", + " print(\n", + " idx[\"model_name\"],\n", + " \":\",\n", + " len(idx[\"regions\"]),\n", + " \"tiles,\",\n", + " len(idx[\"held_out_regions\"]),\n", + " \"held out, unit =\",\n", + " idx[\"unit_id\"],\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "30005ac8", + "metadata": {}, + "source": [ + "## 5. Niches of one fold model\n", + "\n", + "Cells of the tiles the model never trained on are clustered in its own\n", + "embedding space. The preprint's recipe is PCA-10 -> L2 -> HDBSCAN(300, 1, eom)\n", + "-> 15-NN assignment; with a few thousand cells the minimum cluster size is\n", + "lowered to 30 and no sampling is needed. Every cell gets the attention-weighted\n", + "margin `relattn`; every cluster gets its composition, empirical association\n", + "and margin with a bootstrap interval over tiles.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "d4874ce4", + "metadata": { + "execution": { + "iopub.execute_input": "2026-10-13T18:19:56.169217Z", + "iopub.status.busy": "2026-10-13T18:19:56.169124Z", + "iopub.status.idle": "2026-10-13T18:20:05.205975Z", + "shell.execute_reply": "2026-10-13T18:20:05.205651Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "niches: exit 0\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
clustersharecores_presenttop_typesemp_frac_posshare_aurocrelattnrelattn_lorelattn_hi
0C00.3612Tcell_CD4 38%, Imm_other 20%, Tcell_CD8 13%, E...0.420.74-0.18-0.19-0.18
1C10.3512Tcell_CD4 42%, Imm_other 26%, Tcell_CD8 16%, M...0.240.09-0.16-0.17-0.16
2C20.032Epithelial 91%, Fibroblast 9%, Imm_other 0%, T...1.000.70-0.16-0.16-0.15
3Noise0.2612Tcell_CD4 45%, Imm_other 19%, Myeloid_CD11c 11...0.460.77-0.17-0.18-0.17
\n", + "
" + ], + "text/plain": [ + " cluster share cores_present \\\n", + "0 C0 0.36 12 \n", + "1 C1 0.35 12 \n", + "2 C2 0.03 2 \n", + "3 Noise 0.26 12 \n", + "\n", + " top_types emp_frac_pos \\\n", + "0 Tcell_CD4 38%, Imm_other 20%, Tcell_CD8 13%, E... 0.42 \n", + "1 Tcell_CD4 42%, Imm_other 26%, Tcell_CD8 16%, M... 0.24 \n", + "2 Epithelial 91%, Fibroblast 9%, Imm_other 0%, T... 1.00 \n", + "3 Tcell_CD4 45%, Imm_other 19%, Myeloid_CD11c 11... 0.46 \n", + "\n", + " share_auroc relattn relattn_lo relattn_hi \n", + "0 0.74 -0.18 -0.19 -0.18 \n", + "1 0.09 -0.16 -0.17 -0.16 \n", + "2 0.70 -0.16 -0.16 -0.15 \n", + "3 0.77 -0.17 -0.18 -0.17 " + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "run(\n", + " [\n", + " \"grass_mil.interpretability.niches_cli\",\n", + " f\"cells_dir={WORK}/cells/fold1\",\n", + " f\"output_dir={WORK}/niches_fold1\",\n", + " \"clustering.params.min_cluster_size=30\",\n", + " \"sample_size=null\",\n", + " \"n_boot=300\",\n", + " \"positive_name=epithelial-rich\",\n", + " f\"hydra.run.dir={WORK}/niches_fold1/hydra\",\n", + " ],\n", + " \"niches\",\n", + ")\n", + "clusters = pd.read_csv(f\"{WORK}/niches_fold1/clusters.csv\")\n", + "clusters[\n", + " [\n", + " \"cluster\",\n", + " \"share\",\n", + " \"cores_present\",\n", + " \"top_types\",\n", + " \"emp_frac_pos\",\n", + " \"share_auroc\",\n", + " \"relattn\",\n", + " \"relattn_lo\",\n", + " \"relattn_hi\",\n", + " ]\n", + "].round(2)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "f52e9064", + "metadata": { + "execution": { + "iopub.execute_input": "2026-10-13T18:20:05.206776Z", + "iopub.status.busy": "2026-10-13T18:20:05.206696Z", + "iopub.status.idle": "2026-10-13T18:20:05.211615Z", + "shell.execute_reply": "2026-10-13T18:20:05.211298Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAACMoAAAKyCAYAAAAuSKBoAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAABcSAAAXEgFnn9JSAAEAAElEQVR4nOzdd3hUZfr/8c+ZmgYJJUDo0tVFbGChhGIBQcWCLosKuxZcFxXrz7WBbd117R3Xr+CqILpiw0UFpalYQAUFBKQIEpTQAmlTn98fcWYZMumTmZT367pCklPvc+YMec4z97kfyxhjBAAAAAAAAAAAAAAAADRwtkQHAAAAAAAAAAAAAAAAAMQDiTIAAAAAAAAAAAAAAABoFEiUAQAAAAAAAAAAAAAAQKNAogwAAAAAAAAAAAAAAAAaBRJlAAAAAAAAAAAAAAAA0CiQKAMAAAAAAAAAAAAAAIBGgUQZAAAAAAAAAAAAAAAANAokygAAAAAAAAAAAAAAAKBRIFEGAAAAAAAAAAAAAAAAjQKJMgAAAAAAAAAAAAAAAGgUSJQBAAAAAAAAAAAAAABAo0CiDAAAAAAAAAAAAAAAABoFEmUAAAAAAAAAAAAAAADQKJAoAwAAAAAAAAAAAAAAgEaBRBkAAAAAAAAAAAAAAAA0CiTKAAAAAAAAAAAAAAAAoFEgUQYAAAAAAAAAAAAAAACNAokyAAAAAAAAAAAAAAAAaBRIlAEAAAAAAAAAAAAAAECjQKIMAAAAAAAAAAAAAAAAGgUSZQAAAAAAAAAAAAAAANAokCgDAAAAAAAAAAAAAACARoFEGQBVMm/ePFmWpa+//jrRoZTr119/VUpKih577LG47nfLli2yLEtTp06N637ri/py/VQWr3flrFy5UjabTW+//XaiQwEAIKEWLVoky7I0Y8aMRIcSVzNmzJBlWVq0aFGNt1HXzl3nzp01ePDgRIcRN1OnTpVlWdqyZUuiQwEAAIiL6rR/4tlmmjBhgizLqvX91FW1ffyN/fwCQENGogyASjPG6K9//avOOOMMHXvsseHpoWSBg7/cbre6du2qv/zlL/rll1+ibm/9+vX6y1/+ol69eiktLU3Jycnq0aOHrrjiCi1fvrzMOB588EFZlqXk5GTl5eVFXaZ169a67LLLdO+99+rAgQM1O/A6KvRBS31J0ijr+tmzZ4+uvvpq9evXT61atZLb7dZhhx2m888/XytWrEhgxPXD0qVLNXnyZB199NFKT09XSkqKjjnmGD300EPy+Xxlrrd69WqNGzdObdu2ldvtVvv27TV69GitXLkyYrklS5aoX79+SktLU+/evTVnzpyo27v55pvVsWNHFRQUlJrXp08fjRo1SrfeeqsCgUDNDhgAgN8MHjy4zA7LRCZVhDpSSSRIrNCHEzVJzgEAAADQ8FmW1aiS3wEAJUiUAVBpb7/9tlauXKnJkydHnX/kkUdqypQpmjJliq688kq5XC49/fTT6tu3r3JzcyOWfeaZZ3TkkUfqmWeeUZs2bXTFFVfommuuUe/evfXqq6+qX79+ZVafePHFF2VZloqLizV79uwy4508ebJ27dqlZ555ptrHjNgp6/rZuXOnZsyYoWbNmun888/X9ddfr5NOOknz5s1Tv3799Prrrycm4Epo166d1q5dq0mTJiUshjFjxujpp59Wq1atdMUVV+hPf/qT9u7dqxtvvFEjR45UMBgstc5bb72lY489Vh9++KGGDx+uG264Qaeffrp++OEHfffdd+HlfvrpJw0fPlx79+7VlVdeKYfDofPPP19ffvllxPbWrFmjRx99VI8++qhSU1OjxnnddddpzZo1euONN2J7AgAAqEf69euntWvX6pxzzkl0KHF1zjnnaO3aterXr1+iQ0ENTZo0SWvXrlW7du0SHQoAAEBcVKf9Q5spfu6//36tXbu23m4fAJA4jkQHAKD+eO6559S6dWsNHTo06vzf/e53EdVN/H6/zjjjDM2fP19PPPGE7r77bknSnDlzdNVVVykrK0tvvfVWqQ7zvLw83X333VGrxaxYsULff/+9Lr/8cs2cOVMvvviirrjiiqjxdOnSRSeccIKef/553XzzzdU8asRKWddPt27dtHfvXjkckX+SfvjhBx1zzDG65ZZbNGbMmHiGWmlOp1O9evVKaAw33HCDxo8fr1atWoWn/fOf/9SQIUM0f/58vfbaa/r9738fnrdx40aNGzdOffv21Xvvvaf09PSI7fn9/vDPM2fOlNfr1cKFC9W+fXvl5+erY8eOeuGFFyLet3/5y180bNgwnXvuuWXGmZ2drbZt2+r555/XBRdcEItDBwCg3klJSUl42yER0tPTS7U5UD+1bNlSLVu2THQYAAAAcVOd9g9tpvjJyspSVlZWvd0+ACBxqCgD1AH79+/XX/7yF7Vu3VopKSk66aST9NFHH0Udy/TgMvKhqhBJSUnq2LGj7r///lLbPrjk+L/+9S8dfvjhcrvd6tGjh1544YVKx7hz50598MEHOuuss2S32yu1jsPh0GWXXSZJ4SF0fD5fuKLIG2+8EfWp0vT0dD300EMRH+6HvPjii5Kkyy+/XGeffbY+++wzbdiwocwYzj33XG3YsEFffPFFpWIuj9/v1xNPPKG+ffsqLS1N6enpOuaYY3TXXXeVu15oaKpoQySFhk86dFiAefPmaciQIWrVqpWSkpLUvn17nXXWWfrss88klbyuQ4YMkSTddddd4SGvOnfuHLGdNWvWaOzYsWrTpo3cbre6deumqVOnyuPxRCx38HX12muv6bjjjlNycrImTJggSSosLNR9992nI488UqmpqUpPT9fhhx+uq666SoWFhRWeu/KuH4fDUSpJRpJ69eqlww8/XFu2bIlaFaWyqvqekaScnBxdfvnlateundxutzp37qzrrrtOe/fujViurNd22bJlGjVqVHhYo6ysLJ166qmaO3duqX3997//1bBhw5SRkaHk5GQdf/zxeuWVVyp9fDfddFNEkowkJScnh99nn3zyScS8v/3tbyouLta///3vqB9YHfxabNu2TS1btlT79u0lSWlpaerevbu2bt0aXuaVV17RsmXL9OSTT5Ybp81m09lnn60FCxZox44dlT4+AEDjUVhYqAcffFDZ2dnhtkvnzp11zTXXlPobbFmWFi9eHP459DVjxgxNmDBBf/zjHyVJf/zjH8PzDi2lvWzZMp111llq0aKFkpKS9Lvf/U6PPfaYjDERy1WlPd25c+dwe/Wwww4L7zvUViir7RcMBvXEE0+oT58+Sk5OVrNmzXTGGWdEbcOGhpzyeDy66aab1LZtWyUlJemkk07SsmXLKnWud+zYIcuydNVVV0VMDw1xevLJJ0dMX7BggSzLKvX3vqptzUOHQarKfdDBKmrTDR48ONxGHzJkSPh1CLVtQ6rSDluxYoWGDh2q1NRUtWzZUuPHjy9VNbMiodeuqKhIN9xwg9q2bavU1FQNGzYs/JTqqlWrNGLECDVt2lTNmzfXpEmT5PV6I7aTm5uru+++WyeeeKIyMzOVlJSkHj166I477lBxcXGp/Xbu3FmdO3fWrl279Kc//UmtW7eWzWYLn98dO3booosuUvPmzdWkSROdcsop+uabb8LDiB0sFveoAAAAB3vppZd08sknq0mTJkpLS9PAgQP1wQcflFouVm2pg9suM2fOVO/evZWUlKTDDjtM//jHP0r1Q1bU/onWl1pWe7Yy/cvBYFBPP/20hg8frvbt28vlcqldu3b64x//qJ9//rkGZ7qkL/OWW27Rscceq2bNmik5OVm9e/fWww8/HLX/tbJ9nBX1ZYd4PB7dc8896tWrl9xutzIzM3XBBRfohx9+iBrvmjVrdNFFF4X336FDB40ZM0ZfffVVeJlobdbKHmfo/kySFi9eHHFvGXrtom1fqnr/8YQJE7R69WqdfvrpSktLU/PmzfWnP/1JBw4ciHrsAIDaR0UZIMH8fr+GDx+uZcuWqX///ho4cKC2bNmikSNHKjs7u8z13nzzTc2fP1+jR49Wdna23n33Xd16661KTU3VNddcU2r5Rx55REuWLNHo0aN16qmn6vXXX9ell16qli1b6qyzzqowziVLligYDOqEE06o1nGGGpMff/yxtm3bpkGDBumkk04qdx2XyxXxu8/n06xZs9StWzf17dtXf/jDHzRz5kz9+9//1j333BN1G6F4Fy5cGBH7okWLNGTIEI0fP77UBxXRBAIBnXXWWZo3b5569Oihyy67TDabTWvWrNG9996rKVOmVLiNynr77bc1evRotWvXTuedd54yMjK0fft2LVmyREuWLNHJJ5+swYMHa8uWLXrxxReVnZ0d/uAnIyMjvJ0lS5bojDPOkGVZGj16tNq0aaNly5bprrvu0ldffaW5c+eWauTPmjVLixcv1ujRo3XKKaeoTZs2kqRx48bprbfe0qBBgzRixAgFg0H9+OOPmjFjhu68806lpKSUe0zVuX62bNmi9evX6/DDD5fNVvO8zsq+Z3JycnTCCSdo+/btOuuss3T44Yfrq6++0qOPPqr58+dr2bJlatKkSZn7Wb58ubKzs9WkSRONHj1arVu31i+//KLPP/9cc+fO1ahRo8LLPvjgg7rpppvUqVMnXXjhhXK73Zo3b54uuugi/fzzz/p//+//Vft4QwkvBye+GGM0Z84cHXPMMerSpYsWLlyor776Sm63WwMGDNBxxx0XsY127dpp165dysnJUdu2bVVQUKAff/xRRx99tKSSD7duvPFG3XLLLeratWuFMZ1wwgl65plntHjx4qiJcACAxm3r1q3661//qiFDhui8885TSkqKvvvuOz355JNaunSpPv/8c7ndbknSlClTNGPGDP30008R7bCjjz5aGRkZ2rdvn95++22dffbZ4b9bBycUz549W+PGjVPz5s119tlnKyMjQx9//LEmT56sH374IerQnZVpT0+ePFkzZszQypUrde2114bbZhWNd3/55ZfrhRdeUK9evXT11Vdrz549evXVVzVo0CC98847Ov3000utc8EFF2jNmjU677zzdODAAc2aNUunnXaafvjhhwpLvGdlZal79+7hZKOQJUuWSCppzxQUFISHVAxNP/j+pDptzYPV5n1Q6AOKxYsXa/z48eHXPnQtSFVrh33zzTfKzs6Wz+fT73//e2VlZWnevHk69dRTS33wUhkXXHCBfvzxR11wwQXatm2b5syZE/7AITs7WwMHDtTll1+uDz/8UE899ZQyMjJ07733htdfsWKF/v73v2vYsGE64YQTZLfb9eWXX+ree+/VihUr9N///rfUPj0ej4YOHSpjjMaOHasDBw7I5XIpLy9PgwYN0o8//qgRI0bo6KOP1nfffafs7OyI81UZVb1HBQAAmDRpkp566ikdccQRGj9+vAKBgN5++22NGDFCr7zyisaOHVtqnZq2pUJmz56txYsXa+zYsRoxYoTeeust3XLLLdq+fbsef/zxSsVfVl9qNJXtX/Z6vZo0aZIGDBigkSNHKiMjQxs2bNDLL7+sBQsW6Ntvv1WLFi0qeYYjvf/++3r22Wc1bNgwDRkyRD6fT0uXLtUNN9ygDRs2RNwHVbaPszJ92VJJAtCZZ56p+fPn6/jjj9f111+vbdu26bXXXtMHH3ygxYsXR7Q/P/roI5155pny+/06++yz1a1bN+3YsUOLFi3Se++9p759+9b4ODt37qwpU6borrvuUqdOnSIS6w/uZz9UdfqPN2/erAEDBqhfv3668sor9fnnn2v69Onat2+f5syZU9mXEAAQSwZAQj377LNGkvnjH/8YMf3VV181kowks3nz5vD06dOnG0nG5XKZFStWhKfv3bvXtGjRwnTt2jViO1OmTDGSTMuWLc2WLVvC0zdt2mScTqcZNmxYpeK84YYbjCSzfPnyUvM2b95sJJkLL7wwYrrf7zennXaakWSmTJkSEc+dd95Zqf0ebM6cOUaSueOOO4wxxvh8PtOyZUvTsWNHEwwGo65z4MABI8mceeaZEdMXLlxoJJnx48dXat8PPfRQ+Bj9fn/EvJ9//jn8c+hchI63rGmHxjF9+vTwtHPOOce4XC6zc+fOiGWDwaDZvXt3qXWjbbe4uNi0a9fOtG3b1mzbti1i3nXXXWckmVmzZoWnha4ru91uPvvss4jl9+7dayzLMuecc06p/eTl5Rmv11tq+qHKu35Ctm/fbqZMmWJuv/12M378eJOenm7S0tLMxx9/XOH2y1PV98y4ceOMJPPUU09FTL/55puNJHPzzTeHp0V7bUPnd+XKlaVi2bVrV/jnlStXGrvdbk477TRTWFgYnl5YWGhOPvlk43A4Sr12VXHWWWcZSebtt98OT9uwYYORZM4++2wzcuTI8P8xoa8LLrjAFBcXh5fftGmTcbvdpkePHubGG280xxxzjLEsyyxbtswYY8w111xjunbtaoqKiioV03fffWckmauvvrraxwUAaLjy8/PNr7/+Wmr6zJkzjSQzY8aMiOnZ2dmmrFva0N//g9tYIb/88otJTU01Rx11lNm7d294ut/vN+eff76RFP5bZ0zV29Pjx48v1YYPidb2mz9/vpFk+vfvbzweT3j6t99+a1wul2nXrl1Eeyt03IMGDTIFBQXh6S+99JKRZO65556o5+RQl156qZEUbnMGg0HTrFmz8Pbnz58fsc/mzZuH29zVbWsuXLgwPC1e90EH7zOkqu2wk08+2UgyH330UXia3+83p556qpFksrOzS+0jmtC5HTJkSET76dprrzWSTNOmTc20adPC0wsKCkxWVpbJyMiIuAZ2795t8vLySm3/b3/7m5FkFi1aFDG9U6dO4TbgoW33W265xUgyU6dOjZj+97//Pfw6HCx0Xmvy2gAAABhjzNy5c8PtwYP7W3fv3m26du1qmjdvHtHejVVbKtR2sSzLfPrpp+HphYWFpk+fPsayLPPtt9+Gp5fX/onWl1rWOpXtXw4EAhH3HSFLliwxdru9VLstdP9RGTt27Ijaj3fFFVcYy7LMpk2bwtMq28dZ2b7sf/3rX+HjP7gv/7///a+RZPr16xeeVlhYaFq3bm3cbnep/uRAIGBycnLCv0c7/qocpzGm3DZ9tO1Xp/9YknnyySfD04PBoDn11FONZVlm69atUfcNAKhdDL0EJNjMmTNls9lKVSS58MILdeSRR5a53kUXXaRjjz02/HtGRobOOussbdy4MWq5vmuuuUadOnUK/37YYYdpwIAB+uabbyoVZ6isY+vWrctc5vvvv9fUqVM1depUTZ48WUcddZQ+/PBDdezYUZMmTZIk/frrr5Kktm3bVmq/BwtVfvnDH/4gqaRSxpgxY7R169ZSZdxD0tLSlJycXKosZb9+/bR27dpKlwJ/7rnnlJycrEcffbTU0EEVPbFbHU6nU06nM2KaZVlq3rx5pdZ/9913tX37dt15553hYXNCQkM1vf7666XWO++880pV+rHZbDLGRK0a07Rp01JxRlOZ6ycnJ0d33XWX7r33Xr344otyuVyaM2dOeIipmqrMe8bj8eg///mPDjvsME2cODFi/dtvv13p6el66aWXKrW/aOfr4Kc9nnvuOQUCAT3++ONKTk4OT09OTtatt94qv9+vN998s0rHGPLKK6/onXfe0YABA3TmmWeGp+/atUuSNHfuXH399deaO3eu9u/fr5UrV2rgwIF67bXXdPvtt4eXP+yww/Tee+8pLS1NzzzzjLxer1577TWdeOKJ+vbbb/XUU0/piSeekM/n00UXXRQeluvaa6+V3+8vFVdoiKialokFADRMqamppYYTlKSxY8cqPT1dCxcujMl+XnrpJRUUFOiBBx6IeErQbreHh0iK1k6qaXu6LC+//LIk6e67746oqNinTx+NGzdO27dvj3rs9957b0R7Y+zYsXI6nZWOJ1S1JVQtZtWqVdq7d6+uu+46NW3aNFxtxuPx6IsvvtDAgQPDFWKq29Y8WLzug6KpSjtsy5Yt+uyzzzRkyBANHTo0vKzdbtfdd99dqf0d6p577lFSUlL49zFjxkgqaStffvnl4ekpKSkaOXKk9u3bF9F+at68uZo2bVpqu1deeaUklfleuf/++0u13WfOnKn09HTdeOONEdOvvfbaKj+pHIvXBgAANB7PPPOMnE6nHnvssYj+1ubNm+u6667Tnj179NFHH5Var6ZtqZDhw4dHDDkaagsaY/Tqq69W6hii9aWWpbL9yzabLeK+I2TgwIE68sgja3Rf1KZNm4hzF3LllVfKGFOq4qRUcR+nVLm+7JdfflmWZem+++6LqDw5YsQIDRkyRF9++aXWr18vqaRKza+//qqrrrqqVAVsm82mrKysmB9nZVW3/7hbt24RQ99alqWLLrpIxhh9++231Y4HAFB9DL0EJNiq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UDcw9BKQYJdeeqlmzJih6dOna8OGDRo4cKC2bNmiOXPm6LTTTtOHH34Yk2FZauq4445T27Zt9fHHH2vKlCk12ta5556rp59+Wtdcc4369u2r7OxsHXvssXI6ndq4caPmz5+v/fv366233pLf79fMmTOVnJysMWPGlLnNSy65RCtWrNBLL72k22+/PTz9448/lsvl0mmnnRax/JdffqkhQ4Zo/Pjx4USc8lx77bWaP3++Zs+erW+//VYjRoyQzWbTunXr9MEHH1R4k3Lttdfq6quv1rHHHquzzz5be/bs0Ztvvqlhw4bprbfeilj2uuuuU05OjrKzs3XYYYepuLhY7733nnbs2KGpU6eGl+vVq5dat26tWbNmKSkpSVlZWcrIyNCkSZOUnJysV199VSNHjtQxxxyjM844Qz179tSBAwf0448/avHixbr77rujDs9zqO3bt+v444/X0UcfrWOOOUatW7fW1q1b9eabbyo1NTWceFGe8q6f6dOn64UXXtDQoUPVuXNn2e12rVu3Tu+//76MMXr++efVrFmziHWMMRFPNcfSP/7xDy1atEiTJk0KD2G0fPlyzZ8/X0ceeWTE9RXNgw8+qI8//lhDhw5Vly5dZFmWFixYoFWrVmnChAnhD22OPfZYPfDAA7rpppvUo0cPDR8+XB06dNCvv/6q77//XsuXL9eyZcsqHOt42LBh+uWXXzRs2DB99NFH+uijjyLmH3300Ro9enT494svvlivv/66ZsyYobVr12rAgAHavHmz3nrrLTVp0kTPPvts1P0Eg0FdddVVGjt2bMSHdU2aNNGECRP03HPPqbCwUAcOHNAHH3yg++67r9Q2Pv74Y7Vu3Tr8FDoAAAe78sortWDBAh177LE677zz5PF4NG/ePLVp0ybq2PSDBw/WG2+8oTFjxui0006Ty+XSWWedpaOOOkonnnii3G63HnnkEe3du1fNmzdXp06ddPHFF6tt27aaPn26Lr74Yh1++OEaOXKkDjvsMO3Zs0c//PCDPvvsM73yyivq3LlztY5j8ODBeuihhzRx4kSdc845Sk5O1qBBg0olm4QMGzZMf/zjHzV9+nT16dNHZ555pvbt26dZs2bJGKP/+7//izpkYix0795dbdu2VU5Ojnr37q0WLVqE52VnZ4cTQ0KVZkJi0das7fugUMy33nqrVq9erbS0tPD5rWo7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hx9+qBYtWmjKlCmJDi/CbbfdpqZNm2rr1q3q1atXosMp086dO3X22Wdr7NixiQ4FB4nF9XP33Xfr7rvvjnFkqE1bt27VxIkT9ec//znRoQAAgDqkPt0HNWSDBg1Su3bt1Lt3b9lsNn311Vf67LPP1LVrV02ePDnR4QEAAAAAAMQcQy8BdcDzzz+vf/3rX9qwYYMOHDigli1b6tRTT9XUqVPVpUuXRIcHAAAAADHHfVDdcP/992v27NnasmWLCgsLlZWVpVGjRmnKlClq1apVosMDAACIKYZeAgAAEokyAAAAAAAAANDgPPzww/rkk0/03XffaefOnSouLlabNm00ePBg3XzzzTryyCMTHSIAAADQoK1YsULz58/Xl19+qS+++EI5OTlyu90qLi6u1vb27dunqVOn6s0339Qvv/yiNm3aaPTo0brrrruUkZER2+AbOBJlAAAAAAAAAKCBadmypQoKCnTUUUepXbt2kqTVq1dr/fr1crlceuuttzRixIgERwkAAAA0XKNHj9bbb78dMa26iTK7d+/WSSedpA0bNqhLly46/vjjtXr1aq1evVrdunXT559/rhYtWsQq9AaPRBkAAAAAAAAAaGA+/fRTHXfccUpKSoqY/swzz+iqq65S27ZttXXrVtnt9gRFCAAAADRs//jHP1RYWKi+ffuqb9++atOmTbUTZS655BK99NJLOvfcczV79mw5HA5J0jXXXKMnnnhCl1xyiV588cVYH0KDRaIMAAAAAAAAADQi3bt3148//qjVq1friCOOSHQ4AAAAQKNgWVa1EmV++eUXtWvXTna7Xdu2bVPr1q3D8zwejzp06KA9e/Zo+/btEfNQNluiAwAAAAAAAAAAxE+oiozL5UpwJAAAAAAqMm/ePAWDQQ0aNKhUIozb7daZZ56pQCCgefPmJSjC+odEGQAAAAAAAABoJP79739r3bp16tGjh7p06ZLocAAAAABUYOXKlZKkY489Nur80PTQcqiYI9EBAAAAAAAAAABqxz//+U+tXr1aBQUFWrt2rVavXq22bdtq5syZstl4jhIAAACo67Zu3SpJat++fdT5oemh5VCxBpUo06ZNGxUUFKhjx46JDgUAAAANxNatW5Wamqpffvkl0aHUG7TLAQBATdWFNthZZ52ljRs3JmTfW7duLbMttXr16ipt64MPPtBHH30U/r1Dhw56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UaNGqU+fPsrJydGbb76pBx54QDNnziy1/vr163X77bfrww8/1IEDB/TFF1+EK6GsWrVKY8aM0d///nft2bNH06ZN08UXX6x169bVKObq2n3Ap4Li6g25dKj84oB2H/BVeb0NGzZo8+bNGjt2bEziAAAAAAAAAAAAAAAAdReJMofIzc1N6PqTJk1Sly5dlJSUpHHjxmnFihX64osvtGPHDt17771KSkrSUUcdpUmTJmnGjBml1rfb7TLGaPXq1SoqKlLr1q111FFHSZKmTZumCRMmaOjQobLZbBowYIBGjRql1157rUYxV1d1Eltivb3Q69WuXbuYxgIAAAAAAICGwW7F9wsAAABAabTJEUskyhyisLCwRuvXtKJMmzZtwj+npqbqwIED+vnnn9W2bVu5XK7wvC5dukQd6qlr16568cUX9eSTT6p169Y67bTT9O2330qStmzZomeffVYZGRnhr7fffls5OTk1irm69hfGNlFmf6G/yuuEhlravn17TGMBAAAAAAAAAAAAAAB1D4kyhwgGgzVaPxCIzVBCB2vfvr1ycnLk8/0vsWTz5s1q37591OUvuOACLVy4UL/++qv69Omjiy++WJLUoUMHXXvttdq3b1/4Kz8/X88880zMY64Mf8DEdnvVeO169Oihzp0769VXX41pLAAAAAAAAKj/LEl2y4rrFw+wAgAAAJHi3S6nTd7wkShzCJutZqfEbrfHKJL/6devn1q3bq0777xTHo9H33//vZ588kmNHz++1LLr1q3T/PnzVVRUJJfLpbS0NDkcDknSxIkTNX36dC1cuFCBQEAej0fLli3T2rVrYx5zZThiXLfKUY3XzrIsPfHEE/r73/+uJ554Qrt375YkrV+/Xpdeeql++ukneb1eFRcXKxgMyu/3q7i4WH5/1avXAAAAAAAAAAAAAACAxCJR5hApKSk1Wj85OTlGkfyP0+nU3LlztWLFCrVp00ZnnXWWrr/+ev3hD38otazX69Udd9yh1q1bq0WLFvr44481Y8YMSdIxxxyjWbNm6fbbb1dmZqbatWunO+64Qx6PJ+YxV0bTFGeMt+eo1nqjRo3SvHnz9N5776lr167KyMjQ+eefr169eikrK0unnXaakpOTtXTpUt10001KTk7WvffeG9PYAQAAAAAAAAAAAABA7ateZkEDlpmZqdzc3BqtX11btmyJ+H306NEaPXq0pJIhgj788MOo602YMEETJkyQJPXu3Vuff/55mfsYOnSohg4dWu0YY6lFk9gmytRkewMGDND7778fdd6iRYuqvV0AAAAAAADUbzxpCAAAACQe7XLEEtfTIZKTk5WWllatddPS0mqlokxD1aKJU6lJsRmqKi3JHvPEGwAAAAAAAAAAAAAA0LBQUSaK9u3ba/369QoGg5Vex2azqX379rUYVcNjWZZ6tE3VN5v213hbPdqmyrKsGEQFAAAAAAAAlLBkyR7nPidL9HEBAAAAB4t3u5w2ecNHRZkoUlNT1aVLF9lslTs9NptNXbp0UWpqai1H1vB0bpWsVumuGm2jVbpLnVpRyQcAAAAAAAAAAAAAAJSPijJlSE9PV48ePfTzzz8rPz+/zOXS0tLUvn17kmRq4Phu6Vq6Zo8OFAWqvG6TZLuO75ZeC1EBAAAAAADEj8fnVyBolOxyUDW3jrHzcgAAAAAJR7scsUSiTDlSU1PVs2dPFRUVKTc3V0VFRQoEArLb7UpOTlZmZqaSk6lkUlNup00Dj2iu5T/maWeet9LrtUp36fhu6XI7KYwEAMDBFq3bqWmLN2lidhcN7tkq0eEAAACgAoFgUPuLSvpEbDZLSU667AAAAAAAqC3cdVfAGKOVOwq14ievfvilUIUev1LcDvVqY9dx/kKdcFgST/nEgNtpU//Dm+mnnUVavz1f7h2fq8XeFWp6YL0cgUL57Sna36SHdjc7Tp6sE9WjXRrDLQEAUIZpizdp2abdkkSiDAAAQD1gsyw5bDYFTFCOSg4FDgAAAAAAqodEmXK89tU2PbN4ozbvKig1792VJd8Pa5mqP2d31QV9O8Q5uoap08//UcdPHpG1Z2Ppmb+8J0kyzbvKGnCd1OriOEcHAED9MDG7S8R3AAAA1G2WZalZWlKiw0AUliR7nB+S45E8AAAAIFK82+W0yRs+EmWi2FPg1bWvfqOlG3ZVuOzmXQW6+Y1VendVjh77/TFqnuqKQ4QNUMFuac5l0saPK/yPx9qzUXpnkrR6jnTu81Jqi7iECABAfTG4ZysqyQAAANQhxhgVeX2yZCnZ7Ux0OAAAAAAANGrUcj3EngKvLpi2rFJJMgdbumGXLpy2THsKvLUUWQNWsFuaPkLa+HHV1tv4sTTjjJL1AQBo4Bat26mxz32uRet2JjoUAAAAVJEvEFSR169Cr0/+QDDR4aAqLMke5y8eXwUAAAAOQZscMUaizCGuffUb/bgzv1rrbtiZr2tf/SbGETUCcy6Tdq2r3rq5P5SsDwBAAzdt8SYt27Rb0xZvSnQoAAAAqCKH3SaHzSaH3Sa7jR5XAAAAAAASiUSZg7z21bYqV5I51NINu/Ta8m012sYnn3yiESNGqFmzZsrIyFCfPn30wAMPyOv1KicnR2eccYZSU1PVsWNH/etf/6rRvhLu65eqXknmUBs/lr55uUabKOucr1+/Xuecc47atGmjjIwM9e/fX59++mnN4gUAoBomZnfRSV1aaGJ2l0SHAgAAgCqyWZbSU5OUnpIkyyJRpj6xJNktK65fXCEAAABApHi3y2mTN3wkyvzGGKNnFm+MybaeXbRRxphqrTt37lyNGDFCp59+ujZs2KB9+/Zp9uzZWrNmjXbs2KGxY8eqTZs22rlzp15//XXddNNNWrx4cUzijjtjpE8eic22PnmkZHvVUNE5HzFihL777jvt3r1bEyZM0BlnnKFdu2qWUAUAQFUN7tlKs644UYN7tkp0KAAAAAAAAAAAAPUWiTK/+WLzHm3eVRCTbW3aVaAvN++p8nrGGF1zzTX6f//v/2ny5Mlq2bKlJKlXr16aMWOG/H6/PvnkE91///1KTU3VCSecoHHjxumFF16ISdxx99On0p7YJCdp94/ST59VebWKznl2drauuOIKZWZmym636/LLL5fdbteqVatiEzcAAAAAAADqNLsV3y8AAAAApdEmRyyRKPObFT/tjen2lldjexs2bNDmzZs1duzYqPNXrVqlrKwstW7dOjzt6KOPrr9JG1s/j+32tlV9exWd80N99913OnDggI444ogq7wsAAAAAAAAAAAAAACQWiTK/+eGXAzHd3rpqbC83N1eS1K5du6jz8/PzlZGRETEtIyNDBw7ENva42bkmttv7terbq+icH2zv3r36/e9/r1tvvVVt2rSp8r4AAAAAAAAAAAAAAEBiORIdQF1R6PHHdnveqm8vNOzP9u3b1bVr11Lz09LSlJeXFzEtLy9PTZo0qV6QieaNzVBXNdleRec8JC8vT8OHD9eAAQM0derU6kYIAAAAAACAesSSZLfiW3udSu8AAABApHi3y2mTN3xUlPlNiju2OUMprqpvr0ePHurcubNeffXVqPOPOuoo5eTkaOfOneFp3377rXr37l3tOBPKlZrw7VV0ziVp//79Ov3003XkkUfq2WeflRXnzhEAAAAAAAAAAAAAABAbJMr8pleb2FZl6VmN7VmWpSeeeEJ///vf9cQTT2j37t2SpPXr1+vSSy+Vw+FQ//79deutt6qwsFBffvmlXnnlFV166aUxjT1uWh0R2+21rvr2KjrnP/30k04//XT16NFDzz//PEkyAAAAAAAAjYzdiu8XAAAAgNJokyOWSJT5zXGdmsV0e8dXc3ujRo3SvHnz9N5776lr167KyMjQ+eefr169eikrK0uzZs3S9u3blZmZqfPOO08PPPCAsrOzYxp73HQ8Mbbb61C97ZV3zhctWqTPP/9cb7zxhpo2baq0tDSlpaXplVdeiW3sAAAAAAAAAAAAAACg1sV2vKF67ITDmuuwlqnavKugxtvq0jJV/Q5rXu31BwwYoPfffz/qvHbt2mnevHnV3nad0qm/1LyrtGdjzbfVopvU6eRqr17eOR8/fny1twsAAAAAAID6y1L8nyjlAVYAAAAgUrzb5bTJGz4qyvzGsiz9ObtrTLZ15eCuDNFTGZYlDbguNtsacF3J9gAAAAAAAAAAAAAAAMpAosxBLujbQQO7t6zRNgZ2b6kLju8Qo4gagWMvlroOrdk2ug6VjrkoNvEAAAAAAAAAAAAAAIAGi0SZQzz2+2PUvVVatdbt3ipNj/3+mBhH1Aic+7yU2at662b2KlkfAAAAAAAAiDVLsltWXL+o8w4AAAAcIs7tctrkDR+JModonurS7IknVbmyzMDuLTV74klqnuqqpcgasNQW0oT/Vr2yTNehJeultqiduAAADd6idTs19rnPtWjdzkSHAgAAUK/5A0EVeX0yxiQ6lAhev19784tU6PElOhQAAAAAAFBHOBIdQG2zLEuWZckYI7/fL5er4kSW5qkuvXTpCXpt+TY9u2ijNu0qKHPZLi1TdeXgrgy3VFOpLaSL35S+eVn65BFp949lL9uimzTgujo33FIwGAz/bFmkGQJoPBat26lpizdpYnYXDe7ZKtHhVMm0xZu0bNNuSap3sQMAANQleYUeBY1RIGiUllR3HiLy+gIKGiOv368UtzPR4aAesiTZ49zNQ68SAAAAECne7XLa5A1fo0iUcblc8ng82r59u9q1ayeHo3KHff6x7XTeMW311Za9Wv7TXq375YAKvQGluOzq2aaJju/UTH07N5NlWRFJEqiBPn+Qjhorbf1M2vaFrJ1rJG+B5EqVaXWE1OEEqePJkmVJdeyc795d8kGr0+kkUQZAo1Kfk00mZneJ+A4AAIDqcdht8voDctjrVvHiZLdTsiy5HfZEhwIAAAAAAOqIBp8oI0lt27bV1q1bVVxcrI0bN1Z5/XRJw7KkYVnJB031S95crV+fG7M4cbCWUubIkq+DFUtavz4hEVVWq1b160NiAKip+pxsMrhnq3qX3AMAAFAXNU12yUiy1bEHR+w2W52qcIP6yV7HrmsAAACgMaJdjlhqFIkySUlJ6tixo3JycuT1euvceNloGGw2m5o2baqmTZsmOhQAiCuSTQAAAGBZFqWpAQAAAABAvdAoEmWkkmSZLl26yBhDogxqhWVZDLkEAAAAAAAAAAAAAEAd1mgSZUJIZgAAAAAAAABQGZYke5y7Eum5BAAAACLFu11Om7zhsyU6AAAAAAAAAAAAAAAAACAeGl1FGQAAAAAAAACoHEv2uFen5vlVAAAAIFK82+W0yRs6KsoAAAAAAAAAAAAAAACgUaCiDAAAAAAAAABEYUmyx/lhUp5dBQAAACLFu11Om7zho6IMAAAAAAAAGhxjjHyBgIwxiQ4FAAAAAADUIVSUAQAAjdKidTs1bfEmTczuosE9WyU6HAAAAMRYfrFPxT6/3E67mia7Ex0O6itLsluUlAEAAAASKt7tctrkDR6JMgAAoFGatniTlm3aLUkkygAAADRARib0AwAAAAAAQBiJMgAAoFGamN0l4jsAAAAaliZJLiU5g3LaGXkcAAAAAAD8D4kyAACgURrcsxWVZAAAABowy7LkctgTHQYaAFu8h14CAAAAUArtcsQSj9QAAAAAAAAAAAAAAACgUaCiDAAAAAAAAABEYUmy7PF9cpXnZAEAAIBI8W6X0yZv+KgoAwAAAAAAAAAAAAAAgEaBijIAAAAAAAB1gDFGhR6fbDZLyS5nosMBIEmWZItzRRkeXwUAAAAOEe92OW3yBo+KMgAA1JJF63Zq7HOfa9G6nYkOBQAAAPWA1x9Qsc+vQo9PgWAw0eHUS4Gg0YEirzy+QKJDAQAAAAAAdRSJMgAA1JJpizdp2abdmrZ4U6JDwW9IXgIAAHWZ026Xw2aT02GXzaqfj68FjVGxz6+gMQnZf5HXp2KfXweKPQnZPwAAAAAAqPsYegkAgFoyMbtLxPeGbtG6nZq2eJMmZnfR4J6tEh1OVKHkJUl1NkYAAPA//kBQQWPkctgTHUpc2GyW0lOTEh1GjeQXeeXxB+Ry2JWe4o77/l0Ouzy+gNzO0tdMIBiU1x+Q2+mot4lISARLlj3ezxpyfQIAAACR4t0up03e0JEoAwCoNJ/Pp9zcXGVmZsrpdCY6nDpvcM9WjSoZoz4koVSUvFQfkn0AAGgsgkGjvMJiSVKTZHejSZap72y2ks7ERCWiuBx2tWiSHHVefrFX/kBQgUBQacnxT+IBAAAAAAB1A4kyAIBKy83N1Y4dOyRJbdu2TXA0qGvqQwWdipKX6kOyDwAAjYYlWZZkjGTjQa56I9XtVLKrblZssdts8geCsse9OgjqNUuy7HG+nuve2wcAAABIrHi3y2mTN3gkygAAKi0zMzPiO3CwhlBBpz4k+wAA0FjYLEsZqcmS+V+VEtR9lmXJXgeTZCQpLcmlFLezTibxAAAAAACA+CFRBgBQaU6nk0oyaNAaQrIPAAANic2yeIqrEoJBo2KfXy6HXQ6qpZSLJBlUlSXJFueKMlylAAAAQKR4t8tpkzd89J4AAFCHLVq3U2Of+1yL1u1MdCgAAACoowo9XhV5fcov9iY6FAAAAAAAUM/8+uuvWrJkiX799deI6Zs3b9bYsWP1u9/9TiNHjtSXX36ZoAhjj4oyAADUYdMWb9KyTbsliUonAAAAiMpht8vjD8hJNRkcwucPSJbktNsTHQoAAAAAoI76+9//rscff1xr1qxR69atJUn5+fkaMGCAfvnlFxljtGbNGi1ZskQrV65Uly5dEhxxzdGDAgBAHTYxu4tO6tJCE7Prf6MDDQ8VjwAAqBuSXA41T0tWapIr0aGgDvEHgtpX6NG+Ao/8gWCiw6m/LMmy2eL6RZ13AAAA4BBxbpc3tjb5okWLdPjhh6tnz57haTNmzNCOHTs0duxYrVu3To888ogKCgr04IMPJjDS2KGiDAAAddjgnq2oJFNHLVq3U9MWb9LE7C6N9jWi4hEAAHWHZTWyXrxqCgSDKvT45LTblORylppf4PGpyOtTWpJLSc763W128CXB5QEAAAAAKMv27dt14oknRkybO3euHA6HHnvsMbVo0ULXXnutXnzxRS1cuDBBUcZW/b7jBwAAqAWVSYIhSUThSkdUPAIAAPWFx+eX1x+Q1x+Imijj9flljOT1Bep9oozdZlOLtGRJks1GpkxN2OycPwAAACDRaJfXngMHDqhJkybh340x+uKLL3TcccepRYsW4ek9e/bU3LlzExFizNXvO34AAIBaUJkkGJJEqHgEAADqH5fDIV8gKKfdHnV+WrJLHl9Aya6G0WVGggwAAAAAoCLt2rXT5s2bw78vX75ceXl5Gjx4cMRyfr9fLlfDGPa5Ydz1AwAAxFBlkmBIEgEAAKh/HHab0lOSypzvtNvLTKJBI2VZsuL95CpjZQEAAACR4t0ub2Rt8pNOOkmzZs3S22+/rSFDhujee++VZVk688wzI5Zbu3at2rVrl6AoY4tEGQAAgEOQBAMAAAAAAAAAABqD2267TXPmzNG5554rqWTopSFDhujkk08OL7NlyxatWbNGl156aaLCjCkSZQAAAAAAAACgDJbdlugQAAAAgEaPdnnt6dWrlz755BM99thjys3N1XHHHaebbropYpkPPvhAffr00ejRoxMTZIyRKAMAAAAAAFBPBYNGHr9fLodddhudhgAAAAAAoOqOOeYYzZgxo8z5EydO1MSJE+MXUC2jBwUAgDpo0bqdGvvc51q0bmeiQwEAAAkQCAZ1oMijYp8/0aGgjivweFXo8Sm/2JvoUFCLjDEKBk2iwwAAAAAAoEGgogyARsHn8yk3N1eZmZlyOp2JDgeo0LTFm7Rs025J0uCerRIcDRqiRet2atriTZqY3YVrDADqII/PL68/IK8/oCQnt+4om8Nuk9cfkINqMg3a/iKvvP6A0pKcSnbF957WGKNCj082mxX3fdcFliXZ7Fbc9wkAAADgf+LdLm/MbfKtW7dqx44d8ng8ZS4zaNCgOEZUO+htA9Ao5ObmaseOHZKktm3bJjgaoGITs7tEfAdirS4nY5HEAwCSy+GQ1x+Uy0HyA8qX7HIqyemQ1x9QkcenJJdDVmPu0WtgjDEq9gXkDwQkSYEEVJXx+gMq9JZUt2KILwAAAABomF544QXdc8892rp1a4XLBn67R63PSJQB0ChkZmZGfAfqusE9W2lwz1bhIZhIGEAsHJyAUpeTsepyEg8AxIvDblNGalKiw0A9ETQmPPSSzWbJTRWiBiO/2Kdin192m6UmbqfcTnvcY3DYbbLbLNmskq/GyLI1zuMGAAAA6hLa5bVn+vTpuuyyyyRJvXv3Vo8ePZSWlpbgqGoXPScAGgWn00klmQRj+KvqIWEAsXTw9TTrihPr7DVVl5N4AAB1W5HXJ58voJQklxz2xlP1wmZZcthsChrTqI67IoFgUEVev1wOu1yO+CeYhPj8AQWNqVYCk/23jmCHzaYkV2K68ew2m5qnJSdk3wAAAACA2vfwww/L4XDojTfe0JlnnpnocOKCRBkAQFww/FX1kDBQe+rL8D6xjLO+XE+hikoAAFRVsccnI8nj88thdyU6nLixLEvpVCAqpcjrl8fnl88fkCtBiR7BoNG+wpJx3ZtKVU6WSXGXDK11cCGXQNCowOOV025XcoKSZxoXS7a4J6DxpCwAAAAQKd7t8sbVJt+wYYMGDRrUaJJkJBJlAABxwvBX1UPCQO2pL9V6YhlneddTfUkcAgCgPClJLnn9gYRV3kDd4nLY5fMHEjJcUYhllVT8CRojWzXLhB+6XrHPL48vII8voCSnXVYjHQ4JAAAAABAbzZs3b/BDLR2KniMAQFww/BXqmvpSXSVecdaXxCEAAMrjdjqqNbwNGiaXw56wSjIhlmWpeVqSjEoSZmLB7bDL6wvI6bCRJAMAAAAAqLGzzz5bb7/9toqKipSc3DiG3qX3CAAANEr1pVpPvOKsL4lDAAAA9Y1lWTEt2u2w29QsjaG24saSLHucE5JisLvCwkJ9+OGHevfdd/XVV19py5YtCgQC6tatm8477zxdf/31je6JUQAAANRj8W6XN7JnEv72t79p4cKFmjBhgqZNm6aMjIxEh1TrSJQBAKAcDEeDxqK+JA4BAFCfBIJB7S/0yLKk9JQkqn8AiJuZM2fq8ssvlyQdeeSRGj58uPbv36/PPvtMU6ZM0axZs7R48WK1asU9AAAAANDY3XDDDTriiCP0n//8Rx9++KGOP/54tW/fPmo/hmVZ+r//+78ERBlbJMoAAFAOhqMBAABAdQWCRkFjJFPysyPeVSkA1Jil+FeUicXeXC6X/vznP+u6665T9+7dw9N37NihkSNH6ptvvtHkyZM1c+bMGOwNAAAAqF3xbpc3trv3GTNmhH/Oy8vTRx99VOayJMoAANAIMBwNAAAAqstptynF7ZRlWXLYbYkOB42U1x9QIGiU5LRT1agRueSSS3TJJZeUmp6VlaWnnnpKJ598subMmSOv1yuXy5WACAEAAADUFQsXLkx0CHFHogwAAOVgOBqgfAxPBgBA2SzLUrLLmegw0MD4A0H5g0G5HRUnvgSNUV6hR5JkyaUkF12BVWZJtngnutVyPlOfPn0kSR6PR7t371ZWVlbt7hAAAACoqXi3yxvZMwbZ2dmJDiHueJwJAACgkVu0bqfGPve5Fq3bWeV1Q8OTTVu8qRYiA4Da4fUHlF/kUSAYTHQoAFAlxhjtKyjWgSKvin2BCpe3JDlsJT28dob+wm82bSppuzudTjVv3jzB0QAAAABA/PEYCYDoNiyQPn1U6j9Z6n5KoqMBANSiULKLpCpXhWF4MgD1UWGxV0FjZFlSapI70eEAaGD8gaCCxsjlsNfK9m02S4Ggkd1WceKLZVnKSE0K/4zqsRpYktFjjz0mSRo+fLjcbv4OAgAAoH5oaO3yusjn8+nNN9/U0qVLlZOTI8uylJWVpYEDB+qcc86R09lwquaSKAMguk8flbYsLfmZRBkAiItEDWNUk2QXhicDUB+5nQ55/X65HNwSA9Xl8wdkWZYc8R6Spo4LBo3yCoslSU2S3TFPlrEsS81Sk2RMScJMZddB/bNx40YdeeSRUeetXr262tv973//q//7v/+T0+nUPffcU+3tAAAAAGhYPv30U/3hD3/Qzz//LGNMxLynn35aHTp00MyZM3XyyScnKMLYolcQQHT9J0d+Bxq4RCUo1Ceco9pXk8ouNUGyC4DGJtntVLK74TwBg7rBGKP8Yp+MMWqS7GrQyQlef0B5hR5JUosmybI14GOtMkuyLJUkstTSebEsS5xyVMfatWt10UUXyRijf/7zn+rTp0+iQwIAAABQB6xfv14jRoxQfn6+jjvuOF100UXq3LmzJOmnn37Syy+/rOXLl2vEiBFavny5unfvntiAY4BEGQDRdT+FSjJoVBKVoFCfcI5qH8MYAQBQfwWCRsU+vyTJ7Q/I7Wz4XS4ka5RmsyxlpCZLVaj4gjrOsuL/WlqWunbtWqPKMYf6+eefNXz4cO3du1fXX3+9rr322phtGwAAAKh18W6XN7Ib3vvuu0/5+fl65JFHot4rXHPNNXr88cc1efJk3XfffZoxY0b8g4yxht9rAwBAJZCgUDHOUe2jsgsAAPWX3WYpyemQMSbmw+3UNS6HXc3TkmTJoppMFDbLkjgtqEN27dqlU089VVu3btUf//hHPfjgg4kOCQAAAEAd8tFHH+mYY44pN6H+mmuu0YsvvqgFCxbEMbLaQ6IMAAAiQaEyOEcAAABlsyxLTZJdiQ4jbuw2W6JDAOLCkmTZ43u9xzLP6sCBAxoxYoR++OEHnXvuufrXv/7VoIeGAwAAQMMU73Z5Y2sx5+bmKjs7u8LlevXqFdPKl4lErwaAOsfn8yknJ0c+ny/RoQAAAADVUujxam9+kbz+QKJDAQA0Uh6PR2effbaWL1+u008/XbNmzZLd3rArXgEAAACouhYtWmj9+vUVLrd+/Xo1b948DhHVPhJlANQ5ubm52rFjh3JzcxMdCgAAAA4SCARJ/Kgkjy+goDHycb4AoH6zJJvdiutXLB5fDQQCGjt2rBYuXKiBAwdqzpw5crkaT9UrAAAANDBxbpc3tpIyQ4YM0ddff61//etfZS7zr3/9SytWrNDQoUPjGFntYegloDwbFkifPir1nyx1PyXR0TQamZmZEd8BAACQeMYY5RUWS5JSk1xyO0vfTgaDRkVerxx2e9T5jUlqkktef0BJrsZ9HgAAifHkk0/qzTfflCS1bNlSV111VdTlHnzwQbVs2TKeoQEAAACoY26//Xa99dZbuvLKKzVz5kz94Q9/UOfOnWVZljZv3qxXXnlFS5cuVUpKim677bZEhxsT9NgB5fn0UWnL0pKfK0iU+XWfR+tzCtSjbapaZ7hrP7YGzOl0qm3btokOAwAAAIewWZaCxshmRX+sxuPzy+MLyOMLNPpEGZfDLpeD4S0AAImxd+/e8M+hhJlopk6dSqIMAAAA0MgdfvjheueddzRu3DgtXrxYS5YsiZhvjFHr1q31yiuv6PDDD09QlLHVuHsugYr0nxz5vRzrcwq0a79PUgGJMgAAAGhwLMtSemqSjDGy2aKP4ut02OX1++Ww160EkWDQqNDjlcNuU5LLmehwgEap0ONTIBhUapKrzGQ7oK6y7PXvmp06daqmTp2a6DAAAACAmKmP7fL6ZNiwYdq0aZNee+01LV26VDk5OZKktm3bauDAgbrggguUkpKS4Chjh0QZoDzdT6n0kEs92qZKKvjtez3EMFMAatGidTs1bfEmTczuosE9WyU6HABANVmWJaucD7gddpvSU5PjGFHlePx+ef0Bef0llW7KOwYAsRc0RgUenyTJYfcrmYQ1AAAAAADqnJSUFE2YMEETJkxIdCi1jkQZIEZaZ7jrdyWZKgwzBQBVNW3xJi3btFuSSJQBAMSdy2GXzx+Q3WYjSaaRChqjIo9PTodNLgddIfFmsywlOR3yB4MMSYb6x7Jk2aNXUqvNfQIAAAA4SLzb5bTJGzx6hwCUqMIwU0Bt8Pl8ys3NVWZmppxOnjBtaCZmd4n4DgBAPNltNjVNSUp0GEigYq9fxT6/in1SiyZ0hSRCk2RXokMAAAAAAACStm7dKklq166d7HZ7+PfK6tixY22EFVf0DgEoUYVhpoDakJubqx07dkgqGe8QDcvgnq2oJAMAABLG6bDJ47PkjHdVCAD1niXJZo/v06Q8uwoAAABEine7PJZ7Ki4u1v33369Zs2Zp69atat68uYYPH667775b7du3r9K23n//fT322GP66quvlJeXp2bNmqlfv3667rrrNGzYsEpvp3PnzrLZbFqzZo169Oihzp07V7oKs2VZ8vv9VYq7LiJRBgBQJ2RmZkZ8BwAAQHwFjZExRnZbw0smcdrtapaWnOgwABlj5A8aOWwWQ8EBAAAAQANXXFysYcOG6bPPPlNWVpbOPvtsbdmyRdOnT9fcuXO1bNkyde3atVLbevjhh3XDDTfIsiz1799f7dq106ZNm/Tee+/pvffe0zPPPKMrr7yyUtsaNGiQLMtSSkpKxO+NCYkyABBnDDEUndPppJIMUAmL1u3UtMWbNDG7C1V6gHouEAzKsizZyrgJDQSDKvL65XLY5XLY4xwdGhtjjPYVFMkYqWmyW06uuXolv9irQNCoSZJLNlvj6tiqb/IKPfIFgkpxO5Xq5n4QAAAAABqyv/3tb/rss8900kkn6cMPP1RaWpqk/yW9/OlPf9LixYsr3E5ubq7++te/yuVy6aOPPtKAAQPC89544w2NGTNGN9xwgy666KLwPsqzaNGicn9vDBreY2KIq1/3ebR0zR79us8T02WBhiw0xFBubm6iQ0Edt2jdTo197nMtWrcz0aGgDpm2eJOWbdqtaYs3JToUADXg8we0r6BYeQXFMsZEXabI65PH51dBsTfO0dUvZZ0/VI2RFDqVRpzT+iQYNCry+uX1B+TxB2K2XWOM/IFgzLaHEqF3Vyz/7/IHggoGa7a9QDCogmIvr3k0lmTZrLh+MfYSAAAAcIg4t8tj0Sb3+Xx64oknJElPPfVURALL9ddfr6OOOkpLlizRihUrKtzWF198Ia/Xq6FDh0YkyUjSeeedp6OOOkqFhYVas2ZNzQNvJEiUaeg2LJBmjCr5XgvW5xRo136f1ucUxHRZVB8JSXVfZmamsrKyGGIIFYpnQgRJOfXHxOwuOqlLC03M7lKr+6mta4JrDfVdQbFXeQXFCgRr9kFi6APSoCk7JcHlcMhmWXI7KQQajTFGeQXF2pNfJH8gdskBjZXNspSRkqSmyW65HFxz9YnNZinZ5ZDLYZM7hpWA9hd5tLegWAUeX8y2CSk9xa30FHfMqsl4/QHtLSjWnoKiGiXf5Bf7VOj1K5/kTAAAAACIiU8++UT79u1T165ddcwxx5Saf/7550uS3n333Qq35Xa7K7XP5s2bVy3IRozer4bu00elLUtLfu5+Ssw336NtqqSC377HbllUXyghSSpQ64zK/aeJ+GKIIVRWKBGithMipP8l5UhiOJ86bnDPVnF5jWrrmuBaQ31mjJHH55dUUhHG7qr+cwcup0NNfht2qayhl1wOu1xpydXeR2Pg/y1hyR8wYqSgmrPbbeI01k9pSa6o040x8voDsttsctir9n9WqEAJVZtiy2ZZMR1Oz/yvFFSNuBw2ef0Bhl0rg62K7x9U7JdfftH777+vpUuXauXKlcrNzVVeXp7S09OVmZmpo48+WgMHDtRpp52mrKysRIcLAACAOqC+tctXrlwpSTr22GOjzg9NDy1Xnr59+yo9PV0ff/yxPvnkk4iqMnPmzNGqVat08sknq1u3btWK9d///nellnO5XGrRooX69OmjVq3qd/8+iTINXf/Jkd9jrHWGu9LJGFVZFtVHQhLQcFSUELFo3U5NW7xJE7O71DjhIJ5JOagfauua4FpDfWZZllLcTvmDQbliUOUllh+UHsoYI2NKKk3UdaHhXew2W5XitSxLTZLdCgSDcjv5YBeIxuMPhIdva56WLKuMxLxo0pPd8gWCcjnqV0dkY+N2OpRhWbLZrCq9vodKdjmV7IpNlRugPB988IGeeeYZ/fe//1UgECiVjLd//35t27ZNX3/9taZPny673a6RI0fqz3/+s0477bQERQ0AAABU3datWyVJ7du3jzo/ND20XHkyMjL0/PPPa9y4cRo0aJD69++vdu3aafPmzfrqq680fPhwzZgxo9qxTpgwocr3lMOGDdPjjz+uXr16VXu/iUSiTEPX/ZRaqSSTUBsWlFTK6T+5QR3br/s8Wp9TkuBSk4QiEpKAxiOWlTniVaUE9UdtXRNca6jvkurJh4gFxV55/QGluJ11OmZ/ICiv369ir192m6X01KpV0ClJNiJJBihLqGKVvRpJczabJbeN91d9QBWYWmZJlj3Oiad1P8+1ypYvX64bb7xRS5eWVL4eMGCABg8erH79+qlXr15q3ry5mjZtqry8PO3du1dr167Vl19+qYULF+qdd97RO++8o0GDBunBBx/Ucccdl+CjAQAAQNzFu11uSRs3btSRRx4Zdfbq1asr3ER+fr4kKSUlJer81NTUiOUqcv7556t58+a68MIL9cknn4Snt27dWkOHDlWLFi0qtZ1o7rzzTm3ZskX//ve/1aRJE5122mnq2LGjjDHatm2b5s+fr/379+viiy+W2+3WsmXLtGDBAg0aNEjLly9Xx44dq73vRCFRBvVPLQ8nFW+hBBmv32h/oV8MmQSgsqjMAQAoS+C3IYkCwbo7ZIrH51d+sTf8WaDVED8VPESx1yd/IKgUt1M2G1U6UPtcDruapSbLslSjaiMAUFP9+vVTmzZt9Le//U3jxo0r86na5s2bq3nz5uratatGjRolSdq2bZtefvllPf744+rXr58CgUA8QwcAAACqJVQ9saz78aoOdfzQQw/p5ptv1ujRozV16lR16dJFmzZt0p133qmbbrpJn3/+uf7zn/9UK9aLLrpI/fr10xVXXKF//vOfatKkScT8AwcO6Oabb9bs2bP1xRdfqFu3brrlllv0z3/+U3//+9/19NNPV2u/iUSiDOqfWh5OKt7W5xRo136fmqbY1bKpkyGTyuHz+ZSbm6vMzEw5nXX3yWggXqjMAQAoS1qyWz5/yZBEFd2UJ0qoKyA0hFJ1Kl7UJ8YYFXp8kiS7zaZkN4kyiI/KDGlmjJHHH5DdsqhOApRiybLH+//shvc38eGHH9aVV16ppKSkKq/boUMH/fWvf9V1112nadOm1UJ0AAAAqPvi3S631LVr10pVjilLKNmkoKAg6vzCwkJJUlpaWoXbWrx4sW688UYde+yxev3118MPYPXu3Vv/+c9/1LdvX73xxhv68MMPqzVk6S233KLMzEw988wzUfsQmzRpoqeffloff/yxbrnlFr3xxhu677779Morr+iDDz6o8v7qAnrmUP90P0WaMFfqfop+3efR0jV79Os+T6KjqrYebVPVsqlTv+vYRAOPaE41mXLk5uZqx44dys3NTXQoQL21aN1OjX3ucy1atzPRoQAA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ZWVlu1vktiUrN6uuMaVJwwfQWQMAgBxk25YGlZdIGeys0huptKdmNy2pZfRTxA7mmhMjyd8ZvPB9I2WgkU40YmtweYksdR86MsYo7fmKRuyi7Yzi+0ZGpkd/557vqyHZcmKqPBEr2vcMKDwm8I4yxXD96lVXXaUHHnhAF198sVasWKGTTz5ZkrR582Y99thjeuSRRzRv3jyNGjVKF198ccjVAgAAIHxBr8szc6zm5mYdeeSReuGFFzRixAidfPLJWr9+ve6++2499thjevHFFzVu3Lhu9/Pzn/9c119/vWzb1pQpU3TiiSeqtrZWzz//vF566SU99NBDeuqpp1RaWtqvektLSzVr1izNmjWrX/vJB3SUAfLUsIq4pk4ekrNji3K1o0plZaVGjBiR9QCL4ziqqqrqVUgoqG43hWzJys06884VWrJyc6+ed8fStXpn4w4NLo3RWQMAgBxlW1ZGQjLGGKXSnlJuWm66Z11aohFbEduSE40E2inEtiwNKk1oQCKW0bFOLd1kun8dO5qS2t6UVGMyd9ennuer2U23dXLJJN83qmtoUl1Ds9Ke36vnMtIqdxlj1JB0ldwZfgMQjr333luLFi3S5MmTdd999+lrX/uaJOmJJ57QySefrLlz52rChAl68sknNWjQoJCrBQAAAPrmRz/6kV544QUdeuihWrVqlR588EH95S9/0Y033qja2lqdd955PdpPeXm5/uu//ksbNmzQK6+8ogceeEDPPPOM3njjDY0aNUrLly/Xddddl+VXU1joKANgD5vqklpV3aDKgTHVbk9pQlVZrwM5udpRpTXAkouC6nYThCUrN+uOpWs1e/rYQIMnreOTJPXquK1dZOgmg1Zh/QwDALKvKZVW0y4jlAaWxuVEum7VErFtVZSVZLu0DkUjthThGpfObG9KyjdGfsxRaTyznSxbesl88ufutPycJNr+jNzU7Kbbwl97RyN0/kG3jFHgHWWykP3LSZMmTdLf//53/fGPf9SiRYu0fv16eZ6nkSNH6qijjtLpp5+uSDf/jwYAAEBxCHpdnok1ueu6+tWvfiVJ+vWvf63y8vK2xy6//HLNnz9fzz33nP7617/qc5/7XJf7uuqqqzq8f//999cNN9ygs846S7/73e/0ox/9qEe1bdiwQZK0zz77KBKJtH3fU6NGjerV9rmIoAyAPayqbtCW7a62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55xzJEme52nNmjVasWKFjDE644wz9Lvf/a7dc1zX1Xe+8x395je/kSRNmDBBkydPViwW07p169pmbM+cOVPPPPNM2/NOOeUUPfroo+32FY/H1dzc3Of633jjDX32s5/VlVdeqRtuuKHt/nfffbfd97vyPE/33nuvJOnZZ5/VEUcc0en+582bp3PPPVeWZckYo5qaGg0fPrzH9V166aW65ZZb9ri/q/186Utf0tNPP61p06bJGKNly5bp2GOP1RNPPLHHtieffLJeffVVvfvuuyorK+txXbmkpqZGY8eO1Ve+8hXdc889YZcDAAACxFqUtWjYWIsCAACwLmddHj7W5QCQ5wwA5BlJpqN/vp5++mkTjUaNJLNw4cJ2j3396183ksyECRPM888/v8dzP/zwQzN79myz3377tbv/hhtuMD/4wQ/MwoULzcaNG40kE4/H+1X/SSedZBzHMZs3b+7xc/70pz8ZSWbfffc1nud1ut3mzZvNXnvtZY455hgzevRoI8nU1NT0qr7f/va35j/+4z/Mww8/bDZs2NDtfl5++WUjycyePbvtvvPPP99IMi+//HK7bZ944gkjyTz44IO9qikXzZ4921iWZd58882wSwEAAAFiLcpaNBewFgUAAMWOdTnr8lzAuhwA8hdBGQB5p7MPQcYYc+655xpJ5l//9V/b7nvooYeMJDNs2DCzcePGLve9fPnybo/dnw9BGzZsMLZtm5NOOqlXzzvrrLOMJHPVVVd1u10ikTD/+Mc/+vwhaHfd7efOO+80ksyLL77Ydt8LL7xgJJnf/OY3bfclk0mz//77m5kzZ/arnlyxbNkyI8lcdNFFYZcCAAACxFq06+1YiwaDtSgAACh2rMu73o51eTBYlwNA/rL725EGAHLJlClTJEnvv/9+230/+9nPJElz5szRsGHDunz+YYcdlr3iJM2dO1e+7+vrX/96j5/T0NDQ1trz7LPP7nS7p556Svfff7++973vady4cf2utac+/vhjSdLgwYPb7mv9864zaH/+859r3bp1+tWvftWn49x4442yLEtXXXVVp9scd9xxsixLf/7zn9vuW7ZsmS688EJ95jOf0eDBg1VSUqJJkybpqquuUl1d3R77aJ0DPGvWLG3cuFHnn3++Ro4cqWg0ql/84hdt2x122GEaNWqU7r333n61WQUAAIWDtShrUdaiAAAA4WNdzrqcdTkAoDsEZQAUlB07dkhqmRErSVu2bNFLL70ky7J0xhlnhFmaJOmxxx6TJM2YMaPHz/nDH/6ghoYGTZkyRQcccECH2zQ2Nurf/u3fNGnSJH33u9/NRKk9NmrUKEnS6tWr2+5btWqVJGn06NGSpA8++EA/+tGPdMkll2jy5Ml9Os6ZZ54p27b1u9/9TsaYPR7fsmWL/vznP2v48OGaOXNm2/1XXnmlfvvb3yoWi2nmzJk68sgjtX37dv3kJz/R4Ycfrvr6+g6PV1tbq4MPPliPP/64Dj30UH35y19WaWlp2+OWZWn69On6+OOP9cILL/TpNQEAgMLCWpS1KGtRAACA8LEuZ13OuhwA0B2CMgAKhjGm7UPGZz7zGUnS3/72NxljNHbsWFVUVIRYnVRfX6+//e1vGjVqlIYOHdrj5917772SpG984xudbnP11Vdr/fr1+p//+R/FYrF+19obM2bMUElJiebMmaP33ntP69ev15w5c1RaWtr2Ye/yyy/XwIEDdc011/T5OFVVVZoxY4Y2bNig5cuX7/H4//3f/ymdTuuMM85QJBJpu/8HP/iBampq9Morr+jhhx/WY489pnXr1umCCy7QW2+9pZtuuqnD4/3pT3/SwQcfrHXr1un3v/+9Fi5cqAsuuKDdNl/4whcktVyRAAAAihtrUdairEUBAADCx7qcdTnrcgBATxCUAZD3PM/T6tWrdd555+nFF19UPB7XueeeK0naunWrJKmysjLMEiVJb7/9ttLptCZOnNjj52zcuFHPPPOMIpGIzjzzzA63efXVV3XLLbfonHPO6dVVCJkyfPhwff/739df//pXjRkzRvvtt59ee+01XXPNNRo2bJieffZZ/f73v9fPfvYzDRgwoO15TU1NvT5WazvS++67b4/H7r///nbbtDruuOPatfyUWq4m+cUvfqFoNNrWsnR38Xhcv/rVr5RIJDqtZ9KkSZKkv//97z1/EQAAoKCwFmUtKrEWBQAACBvrctblEutyAEDPRcMuAAD6yrKsPe4bMGCA5s+f3zZ/taP2i2HZvHmzJO2xIO/K/fffL8/zdOyxx2r48OF7PO55nr71rW+poqJCP//5zzNWa2/913/9lw466CA9/vjjsixLJ554oo4++mil02lddNFFmjp1atuHk1tuuUU/+tGPtHnzZg0dOlTf//73ddFFF/XoOP/yL/+i73znO3rooYf0q1/9So7jSJI2bNigF154QRMmTNDnP//5PZ734YcfauHChXr33Xe1fft2+b4vSYrFYu3age7qoIMO0j777NNlPUOGDJHU0oITAAAUF9airEVZiwIAAISPdTnrctblAIC+ICgDIG+dc845kiTbtjVw4ED90z/9k0477bR2HzL23ntvSbmxSN22bZsktUvMd6e7lpq/+MUv9Oqrr+quu+5qe61hOfbYY3Xssce2u++WW27RypUr9eqrr0pqmaV76aWX6uyzz9ZXvvIVPfTQQ7r44os1evRonXTSSd0eY9CgQTr++OP18MMP68knn9SJJ54oqeXDojFmjysFJOmmm27Sf/7nfyqVSvXq9bTO1e3KwIEDJX3ydwsAAIoHa1HWoqxFAQAAwse6nHU563IAQF8QlAGQt+bNm9ftNp/97GclSWvXrlVdXV2oM2gHDRokSdq+fXuPtn/nnXf02muvqby8XKecckqH2yxcuFCWZWn+/Pm655572j22ceNGSdJpp52mWCym6667TocffnjfX0Avbdy4Uddee62+/e1vt80D/vnPf66xY8dq/vz5sm1bJ5xwgpYvX66f/exnPfoQJLW0zXz44Yd1//33t/sQJElnnXVWu21XrFihK664QoMGDdKdd96pGTNmaPjw4YrH45JaZtrW1NR0eJyu2mm2av3w0/p3CwAAigdrUdairEUBAADCx7qcdTnrcgBAXxCUAVDQ9t57b33hC1/QSy+9pAceeED/9m//FlotQ4cOlSR99NFHPdr+f//3fyW1fIgpLS3tdDtjjJ577rlOH3/xxRclSVu2bOlpqRlx5ZVXKpFI6Nprr227791339VRRx0l27YltVzp8fnPf17PPPNMj/d7/PHHq6KiQn/84x9VX1+v9957T2+88Yb++Z//WePHj2+37YIFCyRJ1113XdvVJa2ampraPij21ccffywpN+YbAwCA3MNalLWoxFoUAAAgbKzLWZdLrMsBAO3ZYRcAANn27//+75KkOXPmtM2A7cwLL7yQtToOOOAARaNRvfvuu91ua4xpS8B31lJTkpYsWSJjTIdfo0ePliTV1NTIGNPpFQfZsHz5ct1777264YYb9rhCo7Gxsd33DQ0NHc4S7kwsFtPpp5+uxsZGPfLII7rvvvskqcOWmq0fUvbdd989Hvv973/f7/nE77zzjqRPrkoBAADYHWtR1qKsRQEAAMLHupx1OetyAMCuCMoAKHhf+cpXdMYZZ2jTpk2aNm1aW3p+Vxs3btSFF16os88+O2t1lJWVacqUKfrwww9VXV3d5bbLli3Te++9p6qqKs2cOTPjtbz00kuaNGmSjjzyyIzv2/M8XXjhhTrkkEM0a9asdo8dcMABWrJkiT788ENJ0ocffqilS5fqgAMO6NUxWj/w3HfffXrggQcUiUT0ta99bY/tJkyYIEm666675Lpu2/1vv/22/uM//qNXx+zISy+9JEmaOnVqv/cFAAAKE2vRPbEWZS0KAAAQNNble2JdzrocAIoZo5cAFIV77rlHpaWlmjt3rr74xS9q0qRJmjx5shzH0fr16/XKK6/I8zwdffTR7Z73+OOP64c//GG7+1KplA455JC276+++modf/zxParj+OOP18svv6zFixd3mG5vde+990pqWey3tqDMpMbGRq1cuVLNzc17PPbqq6/q29/+dtv3rfNZjz/+eDmOI0k6//zzdf7553e47//5n//RG2+8oZdffnmPqwCuuuoqnXDCCfrc5z6nww47TM8//7waGhr0n//5n72qf/r06Ro5cqSefPJJSdKxxx7b1rZ0V+eee65uvPFGLVy4UBMnTtTBBx+sjz76SEuXLtUpp5yil156Se+9916vjt3KGKOlS5eqoqJChx56aJ/2AQAAigNr0fZYi7IWBQAACAPr8vZYl7MuB4CiZgAgz0gyff3n64UXXjDnnXeeGT9+vCktLTXxeNyMHj3afPWrXzWPPvqo8X2/3fZ333132/E6+7r77rt7fPwNGzaYSCRijjvuuE63aW5uNoMHDzaSzN///vc+vU5jjBk9erSRZGpqavZ4bPHixUaSGT16dKePdfV1zTXXdHjM2tpaM3jwYDN79uxO67rrrrvM/vvvbxzHMePHj+/V+7erK6+8sq2ee+65p9Pt3n//fXPWWWeZffbZxyQSCfOpT33K/PjHPzbpdLrtPdpV6+s/55xzujz+c889ZySZiy66qE/1AwCA/MRatGdYi7ZgLQoAAJAdrMt7hnV5C9blAIDdWcb0c/geAKBXTj31VD322GP64IMPNGzYsLDLQR/Nnj1bv/nNb/TGG2/0ui0oAABAWFiLFgbWogAAAPmNdXlhYF0OAPkr873aAABd+uEPfyjf93XjjTeGXQr6qKamRvfcc4/OPvtsPgABAIC8wlo0/7EWBQAAyH+sy/Mf63IAyG8EZQAgYJ/+9Kd1zjnn6LbbbtOWLVvCLgd98JOf/ESSdN1114VcCQAAQO+wFs1/rEUBAADyH+vy/Me6HADyG6OXAAAAAAAAAAAAAAAAUBToKAMAAAAAAAAAAAAAAICiQFAGAAAAAAAAAAAAAAAARYGgDAAAAAAAAAAAAAAAAIoCQRkAAAAAAAAAAAAAAAAUBYIyAAAAAAAAAAAAAAAAKAoEZQAAAAAAAAAAAAAAAFAUCMoAAAAAAAAAAAAAAACgKBCUAQAAAAAAAAAAAAAAQFEgKAMAAAAAAAAAAAAAAICiQFAGAAAAAAAAAAAAAAAARYGgDAAAAAAAAAAAAAAAAIoCQRkAAAAAAAAAAAAAAAAUBYIyAAAAAAAAAAAAAAAAKAoEZQAAAAAAAAAAAAAAAFAUCMoAAAAAAAAAAAAAAACgKBCUAQAAAAAAAAAAAAAAQFEgKAMAAAAAAAAAAAAAAICiQFAGAAAAAAAAAAAAAAAARYGgDAAAAAAAAAAAAAAAAIoCQRkAAAAAAAAAAAAAAAAUBYIyAAAAAAAAAAAAAAAAKAoEZQAAAAAAAAAAAAAAAFAUCMoAAAAAAAAAAAAAAACgKPx/p/jJVnEBuvsAAAAASUVORK5CYII=", + "text/plain": [ + "" + ] + }, + "metadata": { + "image/png": { + "width": 900 + } + }, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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CHyXLl3UpUuXW7pnwIABGjBggHsCAXAbL8NDLf2KWh0DAAAAAJBPKGABAAAAAADAtkwZcijnK8+YubgXAAAAAAAAd4bb48f/bhOmaapBgwaaP3++1VFsyzAMzZs3L1/fmZSUpJIlS2rXrl35+l4AAAAAAHB7yDA9cnwAAAAAAAAA2SlQ30VKTU3VW2+9pbp16yogIEDBwcGqW7euxo0bp4SEBEnSxo0b1a1bN4WEhMjX11dhYWEaM2aM0tLSsn3+ggULlJiYqH79+kmS0tPT9fLLL6tOnToKDAxUiRIl1LZtW23ZsiXbZx05ckS9evVS6dKl5efnp4iICL377rsuc6ZOnaq7775bJUqU0MiRI12uZWRkqH79+vr0009v9svjYuLEiYqMjFTRokUVHBysxo0ba/Xq1Tl6Vl46efKkevXqla/vLFKkiIYNG6bo6Oh8fS8AAAAAAAAAAAAAAADsr8BsQZicnKwWLVro5MmTGjNmjJo0aSJvb2/FxcXp/fffl5+fn4YNG6YffvhBFStW1NChQ1W2bFnt2LFDTz75pP744w/NnDnzhu+YNGmSBg8eLA+PP3tpaWlpio2N1fDhw1WnTh2Zpqnx48erVatW2rVrlypWrJjlsx5++GEVL15cK1asUMmSJbV69Wo999xzKlq0qB577DH98ssvevnll7VixQoFBASoY8eOatKkidq3by9JmjBhgkJDQ9WjR48cfb3WrVunQYMG6f7775evr68++OADdejQQRs2bFCjRo1u+jlpaWny8fHJUYbMlC5dOs+edSuioqI0ZswY7dq1S7Vr17YkAwAAAAAAyH+mpIxc/AyimXdRANxBrpgOrbh0zjnu5F9c3kaB+nloAAAAAMAtKDB/4xs1apR2796tdevWKSoqSpUrV1ZoaKg6dOigL774Qv3795ckjRgxQhMmTFDTpk1Vvnx5devWTX//+9+1ZMmSGz5/165d+uWXX9S1a1fnOX9/f61fv159+/ZVRESEqlevrnnz5skwDH355ZdZPuvcuXPat2+f/v73v6t+/foqX768nnrqKdWsWVOxsbGSpP3796tmzZpq1aqVGjZsqBYtWmjPnj2SpD179mjmzJl67733cvz1+uqrrzR48GDVrl1bVapU0eTJk1W5cmUtW7bshveFhobq5Zdf1tChQ1WyZElVr15dknT48GF1797duaJW69at9fPPP7vcu3DhQlWsWFGFCxfWAw88oC+++EKGYei7775zzvnfLQjPnDmjfv36qXjx4vL19VXDhg21fv16l+cahqEZM2Zo0KBBKlKkiO6++269+OKLysjIcM7ZtGmTGjVqpMDAQAUGBqpWrVr6+uuvnddLly6tyMhI/etf/7rVLyUAAAAAACjITMlhGjk+aGAByIkrpqmY5NPO44rJbyYAAAAAYGcFooBlmqYWLFigPn36KCwsLNM5RYsWzfL+xMRElShR4obv2LBhg0qVKqUKFSrccF5KSoquXr16w+cVK1ZMERERmj9/vs6fPy/TNLVmzRodOHBA7dq1kyTVqFFDBw8e1JEjR3TmzBlt3bpVderUUUZGhgYMGKApU6Zkm/lWZGRk6OLFizf1zBkzZsjPz08bN27U559/rtOnT6tRo0YqUaKENmzYoC1btig8PFzNmzfXH3/8IUnaunWr+vXrp549e+rnn3/WSy+9pOeffz7bd3Xp0kU7duzQ0qVLtX37dtWtW1dt27bV/v37XeaNHTtWderU0fbt2zVp0iRNmTJFMTExzs/WqVMn1atXT9u3b9eOHTs0duxY+fn5uTwjs3JXViIiIjI9Dh06dFP3AwAAAAAAAAAAAAAA4M5QILYgPHPmjM6dO6eIiIhbvnfv3r2aPn263nzzzRvOO3LkiEJCQrJ93rBhw1SqVCl17NgxyzmGYWjt2rXq1auXihcvrkKFCsnT01Pvv/++c4vB8PBwTZgwQe3bt1d6eroGDx6sVq1a6Y033lClSpXUuHFjPfLII9q9e7caN26s9957TwEBAbf24f/i9ddf18WLF9WvX79s59auXdvl6zV27Fjdc889ev/9953npk+fri+//FILFizQCy+8oKlTp6p+/frO+6pUqaKTJ0/qqaeeyvI969ev1w8//KCdO3c6twV87733FBsbq/Hjx7uslNW1a1c999xzkqRKlSpp7ty5WrNmjQYOHKikpCQlJCSoW7duqly5siRlWtQLCQnR4cOHs/38AAAAAADAToxcbUEoGXmWBMCdo5AMNSkc5DIGAAAAANhXgShgmTlcnjk+Pl5t27ZVz549neWdrFy+fFmFCxe+4ZwRI0Zo+fLlWrdu3Q3LUKZpaujQocrIyNDatWtVokQJrV27Vs8884xKliyphx9+WJL0xBNP6IknnnDeFxcXp1mzZmnnzp166qmnVLVqVX322Wfq27evXnvtNb311lu38On/67333tP48eP173//+6ZKZg0aNHAZb926VT///PN1n/ny5cuKj4+XJO3bt0/Nmzd3uR4ZGXnD98TFxalIkSLO8tU1TZs21aZNm1zO3XfffS7je+65R7/99pukP1cci4qKUps2bdS8eXM1b95cXbp0UZUqVVzuKVy4sC5fvnzDTNdc2w7yf0VEROjY3hM39QwAAAAAAGA9U5LDzHkBi03DAOREYQ8PvRhc1uoYAAAAAIB8UiAKWKVKlVLx4sWzLMVkJi4uTq1bt1aHDh00a9asm3rHuXPnMr12rVD18ccfa+3atdcVhv7X+vXrtXjxYv3222/OwlPNmjUVFxen119/3VnA+qv09HQNGDBA06dPV/HixfXtt9/qn//8pwoVKqS+ffvqn//8Z/YfOhOTJk3S6NGjtXz5crVp0+am7vH393cZOxwONW/eXDNnzrxublDQf3+KyzBu7ae4blSs+99neXt7X3fd4XA4xx9++KGGDRumb775RmvWrNGoUaP0zjvvaMiQIc4558+fV8mSJW8pIwAAAAAAKPgyWHkGAAAAAAAAbpSb9dfzjWEY6tu3rxYuXKhDhw5lOichIcH5661bt6pZs2bq0aOHPvjgA3l4ZP8x77vvPh0+fPi6FZIyMjI0cOBAffrpp/ruu+9Up06dbJ+VkpIiSfL09HQ57+np6VIa+qvx48crPDxcjzzyiKQ/S09Xr16VJF25ciXL+25k1KhRGjt2rL788ku1bdv2lu+/pl69etq7d6/KlCmjSpUquRylSpWSJFWtWlWxsbEu9/344483fG716tWVlJSkXbt2uZz//vvvVb169VvOWb16dUVHR+urr75SVFTUdYWxX375RfXq1bvl5wIAAAAAAAAAAAAAAABZKRAFLEl67bXXFBERocjISM2cOVM7d+7U0aNH9eWXX6pz586aP3++pD/LOy1btlTnzp318ssv648//tCpU6d06tQpZWRkZPn8Fi1ayMvLy2Xru/T0dPXq1UsrV67UkiVLVKJECeezkpKSnPNmzJih8PBw5/iBBx5QqVKl1L9/f23dulWHDx/W7NmzNX/+fHXv3v26d//yyy/68MMPNX36dOe5pk2b6u2339aBAwc0Y8YMNWvW7Ja+XkOHDtWkSZO0YMEChYeHO3OfP3/+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Z+vWrQoPD9fWrVud50aOHKktW7bo1Vdf1a+//qoVK1bojTfe0NChQ69bASolJUVPPPGE5syZI19fX0lS06ZNNWfOHO3du1fjx49XrVq1VKRIkZv+ml65csW5ClVycrLOnz/vHF+zd+9eNW7cWFWqVNG0adN0/vx5nTp1SqdOnXKWvm5WQECAXn75ZY0aNUrvvPOODhw4oD179mjRokV68cUXJUmVK1dW+/bt9eyzz2r9+vXat2+fnnzySV24cOGGq2L94x//0IoVKzR27FgdPHhQX3zxhZ599ln16dPHuf3gzZg4caIWLlyovXv36siRI5ozZ448PT1Vpcp//2D73XffKSQkRDVr1rylzw8AAAAAAAo+w8z5YUdRUVHq2bOntmzZoqlTpyo9PV2zZs1S8eLFnXNWrlyppKQktWjRwsKkAAAA1vPz8taJgeN1YuB4+Xl5yzAMzW7RN8sSVmTpCprdoq/8vX1c7gMAAPZWyJ0PDwwM1KZNmzR16lTNmjVLw4cPl5eXlypWrKiePXs6C1H+/v5au3athg4dqiZNmqhQoUJ6+OGHNW3atBsWeIKCgtSnTx/FxMSodevWf36gQoV04MABLViwQKdPn1ZQUJDCw8O1cOFC9erVy3nv77//rn79+unMmTMqUqSIatSooW+//dblm0opKSk6cOCAUlJSnOfat2+vTz75RK+99prGjRunsmXL6vnnn9ff//736/KNGDFCHTp0UKNGjZznnn76af3888+KjIxUhQoVFBMT47w2b948DRw4UEeOHFFoaGimn/n3339XnTp1nOPt27dr5cqVkiTT/PO7gkuWLHEWrv53Ra/169erefPmWX5NMzNq1CiVKVNG77zzjl588UX5+vqqcuXKevzx/y6xOm/ePA0ZMkQPP/ywAgICNGTIELVu3VrJyclZPrdt27ZasGCB3nzzTb3xxhsqWrSoevToobfeeuuW8gUFBentt99WfHy8HA6HwsPD9emnn7psFRkTE6Mnn3wyT7ZJBAAAAAAAKMgKFSqkRYsW6a233tLp06cVHh6uwMBAlznly5fX559/roYNG1qUEijYMkxTe6/89/vK1bz95Mn3JgHANoJ9/LTkocGKPXVYyw7t1LnUSype2F9dK9ZRZOkK/P9RAADcgQzzWmungDp27Jhq1aqlnTt33tKqSbej0aNHa+nSpfr5559V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voWBro5bTnumEv+/YFGSpAznU95ITEzkPs+BonZe1JZCoUD16tXTnMd9mDPcfznD/ZczYWFh7LEynzI2NkZCQoLcMSgX8LxVNPG4F0087lTQsQCLKBtiYmLkjpBnlBWmWfkmLxU8PL6FG49v4cbjW3jx2BZuPL6FG49v4cbjW7Dll94pTExMACDdh4Jv3rwBAJQsWTLPMhVlzs7OANiutcXzYs5xH+YM91/OcP/lDHtXyr+K0vOboobnraKJx71o4nEvmgrTcWefx0RERERERERERLnI1tYWABAdHZ3mfOV05XJERERERERERFSwsACLiIiIiIiIiIgoF9WpUwcAEBwcnOZ85fTatWvnWSYiIiIiIiIiItIdFmARERERERERERHlosaNG8PMzAxhYWG4fPmyxnwfHx8AQLt27fI6GhERERERERER6QALsIiIiIiIiIiIiHKRvr4+Ro0aBQAYNWoUEhISVPOWLl2KkJAQeHh4oH79+nJFJCIiIiIiIiKiHCgmdwAiIiIiIiIiIqLCbsaMGTh69CjOnDmDypUro0mTJrh//z7Onz8PCwsLbNy4Ue6IRERERERERESkJUkIIeQOQUREREREREREVNglJiZiwYIF2LZtG/777z+UKlUKrVu3xty5c1GxYkW54xERERERERERkZZYgEVERERERERERERERERERERERKQlhdwBiIiIiIiIiIiIiIiIiIiIiIiICioWYBEREREREREREREREREREREREWmJBVhERERERERERERERERERERERERaYgEWERERERERERERERERERERERGRlliARUREREREREREREREREREREREpCUWYBEREREREREREREREREREREREWmJBVhERERERERERERUYCUlJWHWrFmoUqUKDA0NUb58eQwePBjR0dHZXld8fDzGjRsHOzs7GBgYwM7ODmPHjkV8fLzug+cTuth/8fHx2LZtG3r37o3q1avD2NgYJiYmaNCgAVasWIF3797l4hbIS5e/fx+7e/cujIyMIEkSWrduraO0+Y+u99+9e/cwZMgQ2Nvbw9DQEGXKlEGjRo3w008/6Th5/qHLfXjo0CF88cUXsLS0RPHixWFlZYV27drh2LFjuZBcfpcuXcLChQvRpUsX2NjYQJIkGBoaar2+ongNIdLWmTNn0KZNG5QuXRolS5aEm5sb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MqcjoozMmDEDN27cwPHjx/HFF1+oprdt2xZz5sxhAVYhxR5HiYiIiIiIiIiIiIoWFmARERUBdnZ2ckcgohwaMmQIhgwZgtevX0MIARMTE7kjEVEW+Pn5YevWrfDw8FAryqlWrRrCwsJkTEZERERERERERERERLrCAiwioiJg48aNckcgIh0pWbKk3BGIKBtiYmJQoUIFjenv3r1DSkqKDIkopyRJ0ujhij1eERERERERERERERVtLMAiIiIiyoeyMnSokr+/fy4mIaKcqFatGk6fPg17e3u16X5+fqhTp448oShHhBDo3Lkz9PX1AQCJiYno3bs3jIyMAADJyclyxiMiIiIiIiIiIiIiGbAAi4iIiCgf4tChRIXD1KlTMWLECLx48QKpqak4evQobt++jdWrV8PHx0fueKSFAQMGqP27X79+GstUrlw5r+IQERERERERERERUT4gCSGE3CGIiIiIiIgKq61bt2LWrFmIjIwEAFSsWBHz5s1D37595Q1GREREREREREREREQ6wQIsIiIiogLi2bNnuHv3LlxcXGBgYCB3HCLKpri4OKSmpsLKykruKEREREREREREREREpEMKuQMQERERUcbevHmDPn36wNLSEo0aNcKDBw8AAEOHDsUPP/wgczoiysjixYsRFxcHALC0tGTxFRERERERERERERFRIcQCLCIiIqJ8bsaMGbhx4waOHz8OIyMj1fS2bdti165d8gUjokytWLECFSpUQK9evRAYGCh3HCIiIiIiIiIiIiIiygUswCIiIiLK5/z8/PDLL7+gSZMmkCRJNb1atWoICwuTMRkRZSYqKgrbtm3Dixcv0KJFCzg7O2Pp0qV4+vSp3NGIiIiIiIiIiIiIiEhHWIBFRERElM/FxMSgQoUKGtPfvXuHlJQUGRIRUVbp6emhS5cuOHjwIO7du4du3bphyZIlqFChAvr27YtTp07JHZGIiIiIiIiIiIiIiHKIBVhERERE+Vy1atVw+vRpjel+fn6oU6eODImISBv29vYYM2YMhg0bBiEEdu7cCU9PT7i5ueHGjRtyxyMiIiIiIiIiIiIiIi0VkzsAEREREWVs6tSpGDFiBF68eIHU1FQcPXoUt2/fxurVq+Hj4yN3PCLKgkOHDmHt2rXYv38/KleujMWLF2PAgAF48uQJJk+ejF69euHatWtyxyQiIiIiIiIi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", + "text/plain": [ + "" + ] + }, + "metadata": { + "image/png": { + "width": 900 + } + }, + "output_type": "display_data" + } + ], + "source": [ + "from IPython.display import Image, display\n", + "\n", + "display(Image(filename=f\"{WORK}/niches_fold1/pca_triptych.png\", width=900))\n", + "display(Image(filename=f\"{WORK}/niches_fold1/clusters_overview.png\", width=900))" + ] + }, + { + "cell_type": "markdown", + "id": "77bf446a", + "metadata": {}, + "source": [ + "Left: clusters in the first two PCs of the held-out cells. Middle: the\n", + "attention-weighted margin (red = pushes towards epithelial-rich). Right: the\n", + "empirical association (fraction of cells from positive tiles). The overview\n", + "shows composition, association and margin per cluster side by side; with a\n", + "positive control the epithelial clusters should carry positive margins.\n" + ] + }, + { + "cell_type": "markdown", + "id": "98f3ada6", + "metadata": {}, + "source": [ + "## 6. Cross-fold metaclusters\n", + "\n", + "Each fold model is clustered on its own held-out cells with the same settings;\n", + "clusters are matched across folds by Hellinger distance on their composition,\n", + "never merging two clusters of the same fold. A metacluster that has members from\n", + "`match.min_folds` folds replicates. Shares per tile (each tile labelled once, by\n", + "the model that held it out) are tested at the patient level; the transfer stage\n", + "labels every tile with every model and the basis stage fits one shared k-means\n", + "basis for all models.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "eeed12ba", + "metadata": { + "execution": { + "iopub.execute_input": "2026-10-13T18:20:05.212958Z", + "iopub.status.busy": "2026-10-13T18:20:05.212873Z", + "iopub.status.idle": "2026-10-13T18:20:09.163150Z", + "shell.execute_reply": "2026-10-13T18:20:09.162808Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "metaclusters: exit 0\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
metan_foldsshare_meantop_typesemp_rel_meanmargin_meansign_agree
0130.39Tcell_CD4 24%, Epithelial 23%, Tcell_CD8 19%, ...0.05-0.391.0
1230.38Tcell_CD4 23%, Epithelial 22%, Imm_other 15%, ...-0.07-0.391.0
2310.03Epithelial 91%, Fibroblast 9%, Imm_other 0%, T...0.61-0.161.0
\n", + "
" + ], + "text/plain": [ + " meta n_folds share_mean \\\n", + "0 1 3 0.39 \n", + "1 2 3 0.38 \n", + "2 3 1 0.03 \n", + "\n", + " top_types emp_rel_mean \\\n", + "0 Tcell_CD4 24%, Epithelial 23%, Tcell_CD8 19%, ... 0.05 \n", + "1 Tcell_CD4 23%, Epithelial 22%, Imm_other 15%, ... -0.07 \n", + "2 Epithelial 91%, Fibroblast 9%, Imm_other 0%, T... 0.61 \n", + "\n", + " margin_mean sign_agree \n", + "0 -0.39 1.0 \n", + "1 -0.39 1.0 \n", + "2 -0.16 1.0 " + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "run(\n", + " [\n", + " \"grass_mil.interpretability.metaclusters_cli\",\n", + " f\"cells_dir={WORK}/cells\",\n", + " f\"output_dir={WORK}/meta\",\n", + " f\"labels.label_file={RAW}/tile_labels.csv\",\n", + " \"labels.id_column=patch_id\",\n", + " \"labels.label_column=epithelial_rich\",\n", + " \"labels.patient_column=patient\",\n", + " \"clustering.params.min_cluster_size=30\",\n", + " \"sample_size=null\",\n", + " \"n_boot=300\",\n", + " \"match.min_folds=2\",\n", + " \"transfer.min_cells=20\",\n", + " \"basis.k=6\",\n", + " \"basis.sample_size=5000\",\n", + " \"basis.n_init=3\",\n", + " \"positive_name=epithelial-rich\",\n", + " f\"hydra.run.dir={WORK}/meta/hydra\",\n", + " ],\n", + " \"metaclusters\",\n", + ")\n", + "meta = pd.read_csv(f\"{WORK}/meta/match/metaclusters.csv\")\n", + "meta[\n", + " [\"meta\", \"n_folds\", \"share_mean\", \"top_types\", \"emp_rel_mean\", \"margin_mean\", \"sign_agree\"]\n", + "].round(2)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "57decfa0", + "metadata": { + "execution": { + "iopub.execute_input": "2026-10-13T18:20:09.164187Z", + "iopub.status.busy": "2026-10-13T18:20:09.164097Z", + "iopub.status.idle": "2026-10-13T18:20:09.167149Z", + "shell.execute_reply": "2026-10-13T18:20:09.166865Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "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ufNa/ve//7Xb17t371T/7ZLVa5lyzR5++GHj2rVrmc6Vcl955/UwjHu7p73zPvDy5cuGp6enUbp0aePs2bO29nPnzhm+vr4Z/n8IMANL8QIAAAAAAAAAkA0eHh4qXbp0qvauXbvKy8tLa9euTbXP3d09VVuJEiWy3G/RokWyWq0aOXKkvLy8bO0PPPCABg4cqOvXr+vrr7+WJJ09e1bff/+96tatq379+qUat1y5cumc4W33cp5Z8dlnn8liseidd96xLWMnSW3atFGzZs30+++/66+//kp13AcffKBChQrZHnfv3l2StGvXrnvOkpXreqfXXnvN7vUJDQ1V+fLl7bJs375d27dv14ABAxQSEmJ3fP/+/VW6dGl99dVXd824cOFCOTk5pZrp5fnnn1e1atVS9f/ss89UtGhRvf7663btL7/8sipVqqSlS5cqPj7+rs+bGQMHDlTJkiVtjytVqqQePXro3Llztr+Te70ODRo00NNPP53tjMuWLVNcXJz69eunBx54wNbu5eWl119/XVarVQsXLsxwjIULF6pYsWIaPny4XfuwYcNUvHjxbGcEAGRdTt+v/JOLi4vGjh1rd+9yN4ULF7abIVCSBg8eLB8fH33++ecOzZeRjRs36vjx43r22WdVq1YtW3uRIkX09ttvS1KaS8Y+99xzdjMRe3t7q3379oqMjEx3Cdy7SUxM1JIlS1S+fHm9/PLLdvtGjRolLy+vNLNUqlRJffv2tT22WCx67rnnZBiGdu/ebWufNm2anJ2dNXHiRLv7Rh8fHw0ZMkSXL1/W6tWrM533tddek5ubm+1x48aN9dhjj2nXrl36888/JTnmXiNFTlzzf/Lx8dHgwYPt2t544w0VLlxYixYtsrXd67UcPXq0PD09s50zu/e03377re118fX1tbWXKVNGgwYNynY+wNFYihcAAAAAAAAAgGz67bff9MEHH2jr1q26ePGibt26Zdt39uxZ2z83bdpUpUuXVt++fbVmzRq1adNG4eHhdl/2SdLjjz+uESNGqEOHDurataseffRRhYWFpfqieu/evZKkJk2apMqUshzrnj17JEk7duyQYRhq0aJFlr58vpfzvBd79+6Vn5+fKlasmGpfWFiY1q5dqz179qhKlSq29uLFi6e6dmXLlpUkxcbGZiuLlLnreqc7vxS/M8+d1+b333+XJB05ckRjxoxJ1b9w4cK2L4TvltHPzy/VkoAWi0WNGjXSoUOHbG1Xr17V8ePH1ahRI7tCRUlycnJSaGiojhw5ogMHDuihhx6663PfTaNGjVK1NWzYUNOmTdPevXvVunXre74Od36pnR33+hqnSLmmjRs3TvUltbu7u+rUqaM1a9Y4JCsAIGty8n7ln8qXL5/lYu7AwEDb/UoKNzc31a5dW2vWrNHFixdVqlQpR8ZMU0bvhY0bN1bhwoWzdL8j3b7/+udyuZnx559/KjExUQ0bNrQrFpOkokWLqk6dOlq3bl2qa1OzZs1U97Vp3Qv+/vvv8vLy0rhx41I99+HDh20ZnnjiiUzlTe9eZ8WKFdq7d6+qVq2a7XuNO+XENf+nOnXq2BUrSrd/+BMQEGA7F+ner6Uj7uEccU+bci5pvYaNGzfOdkbA0SjsAwAAAAAAAAAgG9avX69WrVrJzc1Njz32mIKCgmxfik2YMEGJiYm2vt7e3tqyZYtef/11LV++3Db7xUMPPaSPP/5YoaGhkqQKFSpo8+bNGj16tBYsWKCZM2fKYrGoefPmmjhxooKDgyXd/nJLUpoz05QpU8auz5UrVyQp1RfJOXGe9+Lq1asKDAxMc98/zyVFsWLFUvUtXPj2Vx/JycnZyiJl7rpmJs+dWWJiYiRJS5Ys0ZIlS7KVMb3r9c/cGZ2PlPE53Yu0ihFSnjvlOe71OqR3Dll1r6/xP49Pr/DCUTkBAFmT0/cr/3Qv/76/23vH1atXc6WwL6P3wkKFCqlEiRJZut+R7v3+Kyv3Kndem8xmiYmJ0a1bt1LNdHynrMxcnJl7nezea9wpp+5575TR3+XRo0eVnJysQoUK3fO1dMS9kSPuaTO6h+P+DXkRhX0AAAAAAAAAAGTD+++/r8KFC2vHjh2qVKmSrd0wDH344Yep+lesWFGLFy/WzZs3tW3bNn377beaNGmS2rVrp0OHDsnPz0/S7Vktli9froSEBG3dulVLlizRp59+qrZt2+rQoUNyc3Ozfcl3/vz5VLNWnD9/XtLfXwSm7D9z5kyunGdWFStWzJb5n/55LjktK9c1q1JmVPnss8/UrVu3bGW8ePFimvsuXLiQqq+kXLu+Fy9eVNWqVdPMlPIc93od7nW2yX/K6Jpk5nqk5M/sawAAyB05fb/yT/fyvnS3946U9x8nJydJsptxMIUjivEzei9MTk5WdHS03UzJOSmn71WKFi2qEiVK2GaUy66LFy/K39/fru2fr1927zVyW0Z/l+7u7raZFO/1WjriHs4Rfycp+9I6X+7fkBc5mR0AAAAAAAAAAID72dGjR1WjRg27L48ladeuXbp+/Xq6xzk7O6tRo0Z6//339dprr+nq1avasGFDqn5ubm5q3ry5pk6dqoiICJ04ccK2hFTKslybNm1KddzGjRvt+tSrV08Wi0WrV6+WYRg5fp4pX0ZndhaRWrVq6dy5c4qMjEy175/nktOycl2zKmVZsN9+++0e090WEhKis2fPKioqyq7dMAxt3brVrq1YsWIKCgrSH3/8YZu5MYXVatXmzZtVuHBh1ahRw9ZusVjueQaYLVu2pGpLyVSzZk1JjrsO9yq7r7GXl5cCAwP1xx9/KC4uzm7f9evXtXv3bseFBQBkmiPvV7J6L5NZx48fT/VDi4SEBO3evVtlypSxzSTm7e0tKe0fZezatSvbeTN6L9y6datu3bqVa/deVatWlYuLi7Zu3Zoqf1xcnHbv3i0/P797nsnwoYce0rFjx9ItXsuqzNzr5OT9ZHqy8ze7a9cuJSQk2LWdPn1aJ06csJ2T5PhrmRX3ck/7TyEhIZLSfg03b97s2MCAA1DYBwAAAAAAAABANjzwwAM6fPiw3QwPV69e1aBBg1L13bdvn06dOpWqPeVYFxcXSdL27dsVHR2dql/KF2gp/bp27SonJye99957dl9unT59WhMnTpS7u7s6deokSfLz89Pjjz+unTt3aurUqanGvttMflk5T0ny8fHJ1LgpnnvuORmGoddff92u8PDnn3/W6tWr1aBBg2zNGjNmzBhZLBbNnTv3rn2zcl2z6pFHHlHt2rU1ffp0rVu3LtX+q1evZqoorFu3brJarRo9erRd+4IFC3Tw4MFU/Z977jnFxcXpnXfesWufOXOmDh8+rKeeekoeHh62dh8fn3ue3fGTTz7RpUuXbI+PHDmiefPmqUyZMmrevLkkx12He/Xkk0/K09NTU6ZM0cmTJ+2e9+2335aTk9NdZxLs1q2brl69qg8++MCu/aOPPtLly5dzJDcAIGOOvF/J6r1MZqW1jOmECRN0+fJl/etf/7K1Va1aVR4eHvriiy/slhD+/ffftXDhwmznDQ0NVWBgoBYvXqw9e/bY2m/evKlRo0ZJkp5//vnMn1gaLBaLgoKC7trPxcVFTz/9tKKiojRjxgy7ff/9738VGxubrSyvvPKKkpOT1bt37zQLPLdt25bhD3L+6d1337UrgtuyZYtWrlypOnXqqFq1apIcc6+RVdn5m718+bImTJhg1/bWW2/p1q1bdn+Xjr6WWZXVe9p/evLJJ+Xh4aEpU6bo3LlztvYLFy5o4sSJOZYbuFcsxQsAAAAAAAAAQDb06dNHv/zyi+rWratOnTopMTFRK1askK+vr8qWLWvX95dfftGwYcMUFhamqlWrqlixYtq9e7d++uknVa9eXY899pik28uTTp8+Xc2bN1elSpXk6uqqLVu2aPPmzWrWrJlq164tSapSpYpGjx6t0aNHq2bNmurcubNu3rypxYsXKzo6Wv/73/9UokQJ2/NPmzZNf/zxh/r376+vvvpKjzzyiOLi4rRjxw65urpq7dq1DjlPSQoPD9fXX3+tp59+Wo8++qiKFCmi9u3b22bJ+KeePXtq8eLF+uKLL3T06FG1aNFCp06d0hdffKFixYql+pI3q1KKBQsXvvtXI1m9rllhsVj0+eefq1mzZmrevLlatmypBx98UDdv3tTRo0e1bt06Pf/885o+fXqG47zwwguaP3++5s+fryNHjqhp06Y6evSoli1bplatWmnVqlV2/UeOHKlvv/1WH374oXbs2KGHH35Yhw4d0rJly1S2bFmNGzfOrv+dr1/NmjXl5OSkAQMGpFqaOC0hISGqVauWnnnmGSUlJWnRokVKSEjQvHnzVKRIEYdeh3vl7e2tyZMnq2fPnqpTp46effZZubi4aMmSJTpx4oTeeOMNBQcHZzjGyJEjtWTJEo0dO1bbtm1TnTp1tGfPHm3cuFGhoaFpztADAMhZjrxfeeSRR+Ti4qLx48crJiZGPj4+CgwMzHaxW82aNfXDDz8oPDxcjRo10r59+7R8+XIFBgbaCuqk28Vuffr00bhx4/TQQw+pVatWOn36tJYtW6a2bdvq22+/tRu3WrVqKlOmjBYtWiRXV1f5+fnJ29tb/fv3TzNHoUKFNHPmTLVr106NGzfWs88+qxIlSuj777/XgQMH9Pzzz6tNmzb3fJ5ZufeSbi+jvG7dOvXv31+rVq1S9erVtX37dq1atUrBwcF21yarOnTooIEDB+qTTz5RlSpV1LJlS/n6+ur06dPauXOnDhw4oLNnz8rd3T1T4/n5+alWrVrq0KGDoqOjtWjRIhUpUkSTJ0+29XHEvUZWhYWFSZJee+017d+/X56enqpVq5aeeOKJux4bGhqqd999V1u3btWDDz6oLVu2aP369apXr5769Olj6+foa5lVWb2n/afixYtr3Lhx6tOnj2rXrq1nnnlGFotFX375perUqaMVK1bkSG7gnhkAAAAAAAAAACBbFixYYNSsWdNwdXU1ypYta/Tr18+4cuWKERgYaISFhdn6HThwwBgwYIAREhJieHl5Ge7u7ka1atWM1157zYiOjrb127p1q/Hiiy8a1atXNzw9PY1ixYoZISEhxvvvv2/Ex8enev7PPvvMaNCggeHu7m54enoaTZo0MX744Yc0s168eNEYPHiwUb58eaNIkSJGqVKljGbNmhlffvmlrc+cOXMMScbatWvv6TwNwzCSkpKMgQMHGn5+foaTk5MhyZgzZ06G4yckJBhvvvmmUaVKFaNIkSJGiRIljM6dOxsHDhxIdR6BgYFGYGBgmucoyejRo4ddW6dOnQw3Nzfj0qVLaR6Tlsxe1x49ehiSjGPHjqXaFxYWlmbOs2fPGoMGDTIqVqxoFClSxPDx8TFq1aplDBs2LM3zTcvVq1eN/v37G6VKlTLc3NyMRx55xPjll1+M0aNHp5knJibGGDp0qBEYGGg4Ozsbvr6+Rs+ePY2TJ0+mGvvUqVPGk08+aXh7exsWiyXd80vrOhw+fNj4z3/+Y/j7+xsuLi5G7dq1jWXLlqV5TGavw9q1aw1JxujRozN1bVIcO3Yszb+HFCtXrjTCwsIMT09Pw83Nzahfv74xf/78VP3Su6bnzp0znnvuOcPb29vw9PQ0mjdvbuzcudN2LQAAuc9R9yuGYRhLly41atWqZbi4uBiS7I7/5+M7pbz//PN9K+WYyMhI44knnjCKFi1qFCtWzHj66aeNEydOpBrn5s2bxogRIww/Pz/DxcXFqFu3rrF8+fJ076U2bNhgNGzY0HB3dzck2d2DpHX+hmEYv/32m9G2bVvDy8vLcHFxMR588EFj/PjxRnJysl2/lOe88/qkSOt98o8//jAkGcOHD0/zGqXl9OnTxosvvmj4+fkZzs7ORkBAgDF48GDj8uXLdv0yen9PuWdIK+eSJUuMFi1aGN7e3oaLi4sRGBhoPP7448acOXOMmzdv3jVfWFiYIcmIi4sz+vXrZ5QpU8ZwdXU1GjVqZGzcuDHNY7Jzr5HVa24YhjF9+nSjatWqhrOzc4b3QCnuvJbbt283wsPDDQ8PD8PHx8d48cUXU137FJm9linXLKsyur/Oyj1tetfgs88+Mx588EGjSJEiRkBAgDF69Gjj8OHD93S/CeQki2HcMZ89AAAAAAAAAABAPuTn56cOHTpo2rRpZkfJtyIiIjRv3jwdO3YsU8vuAQCA/GvGjBnq37+/jh07Jn9/f7PjOER4eLjWr1+v/FRmExUVpfLly6tHjx6aO3eu2XEA/IOT2QEAAAAAAAAAAABy0tGjR3X+/HkNGTLE7CgAAAAFwubNm/XMM8/km6I+ADBD5hYzBwAAAAAAAAAAuE9VqFBBVqvV7BgAAAAFxvz5882OAAD3PWbsAwAAAAAAAAAAAAAAAAAgD7EY+WnxbwAAAAAAAAAAAAAAAAAA7nPM2AcAAAAAAAAAAAAAAAAAQB5CYR8AAAAAAAAAAAAAAAAAAHkIhX0AAAAAAAAAAAAAAAAAAOQhFPYBAAAAAAAAAAAAAAAAAJCHUNgHAAAAAAAAAAAAAAAAAEAeQmEfAAAAAAAAAAAAAAAAAAB5SGGzAwCO4Ovrq/j4eAUEBJgdBQAAAAAAONCJEyfk4eGhc+fOmR0lR7Rv316RkZEOG69ixYr67rvvHDYe8pb27dtLEq8xAAAoULgHAgAABRWFfcgX4uPjFX89QcdORpsdBWaxmB0AZrEkG2ZHgIl4/Qu4pJtmJ4CJLIUKmR0BJjGsVrMjwEwG7/0F0XXrdSXE3TA7Ro6JjIzUkSMHVSnIOdtjHYni/ii/c2QRKMwXHBwsSdq/f7/JSQAAyNu4BwIAAPciP/x3N4V9yBcCAgJ07GS06jw6zOwoMInBwuIFlsvlW2ZHgImcY/LvF7y4O6djp8yOABNZvL3MjgCTGNfizI4AExkUdRdIm+O+MTtCjqsU5Kw/1gdme5yaYccdkAYAAAAAAABAXkBhHwAAAAAAAGAqQ1Y5YkZSZrUEAAAAAAAA8gsK+wAAAAAAAAATGZKSjewX9hmSLNkeBQAAAAAAAEBewOKVAAAAAAAAAAAAAAAAAADkIczYBwAAAAAAAJjMyjK6AAAAAAAAAO5AYR8AAAAAAABgIkOSVSzFCwAAAAAAAOBvLMULAAAAAAAAAAAAAAAAAEAewox9AAAAAAAAgKkMJRuOWIqX5XwBAAAAAACA/ILCPgAAAAAAAMBkVoryAAAAAAAAANyBpXgBAAAAAAAAAAAAAAAAAMhDmLEPAAAAAAAAMJEhKdkBM/Yx5x8AAAAAAACQf1DYBwAAAAAAAJiMpXgBAAAAAAAA3ImleAEAAAAAAAAAAAAAAAAAyEOYsQ8AAAAAAAAwkSEp2WApXgAAAAAAAAB/o7APAAAAAAAAMJnV7AAAAAAAAAAA8hSW4gUAAAAAAAAAAAAAAAAAIA9hxj4AAAAAAADARIakZAcspMtSvAAAAAAAAED+QWEfAAAAAAAAYLJkqvIAAAAAAAAA3IGleAEAAAAAAAAAAAAAAAAAyEOYsQ8AAAAAAAAwmdXsAAAAAAAAAADyFAr7AAAAAAAAABMZkpJlccg4AAAAAAAAAPIHluIFAAAAAAAAAAAAAAAAACAPYcY+AAAAAAAAwEyGZHXEdHtM2QcAAAAAAADkGxT2AQAAAAAAACZiKV4AAAAAAAAA/8RSvAAAAAAAAAAAAAAAAAAA5CHM2AcAAAAAAACYzBEz9gEAAAAAAADIPyjsAwAAAAAAAExkSLIaLMULAAAAAAAA4G8sxQsAAAAAAAAAAAAAAAAAQB7CjH0AAAAAAACAyViKFwAAAAAAAMCdKOwDAAAAAAAATGTIomQHLKxhUBwIAAAAAAAA5BssxQsAAAAAAAAAAAAAAAAAQB7CjH0AAAAAAACAyawGs+0BAAAAAAAA+BuFfQAAAAAAAICJDEnJDlhG18h+FAAAAAAAAAB5BEvxAgAAAAAAAAAAAAAAAACQhzBjHwAAAAAAAGCyZIPf3wIAAAAAAAD4G4V9AAAAAAAAgKkssjpkYY3sL+cLAAAAAAAAIG/gp8AAAAAAAAAAAAAAAAAAAOQhzNgHAAAAAAAAmMiQlOyA2faM7EcBAAAAAAAAkEcwYx8AAAAAAABgsmTDKdubI/3666/q1KmTfH195ezsLB8fH7Vo0UJLlixJ95j58+erQYMG8vT0lI+Pj9q2bastW7Y4NBcAAAAAAABQUFDYBwAAAAAAAMDmq6++UuPGjbV06VI98MAD6tSpkx588EGtW7dOTz/9tEaOHJnqmKFDh6pHjx7at2+fWrZsqQYNGmjVqlVq2rSpvvnmGxPOAgAAAAAAALi/UdgHAAAAAAAAmMiQZJUl25sjluK9deuW+vXrJ6vVqsWLF2vbtm1avHixNmzYoE2bNsnV1VUffPCBIiMjbcesWbNG48ePV4kSJbRnzx4tW7ZMK1eu1IYNG1SoUCH17NlTMTExDkgHAAAAAAAAFBwU9gEAAAAAAACmsihZTtneJEu2kxw6dEgXL15UtWrV1KVLF7t9DRs2VOvWrWUYhnbs2GFrHzdunCRp1KhRqly5sl3/Pn366MqVK5o9e3a2swEAAAAAAAAFCYV9AAAAAAAAACRJLi4umern4+MjSbpx44ZWr14tSercuXOqfilty5cvd1BCAAAAAAAAoGCgsA8AAAAAAAAwkSEp2XDK9uaIpXgrVKigChUq6NChQ/ryyy/t9m3dulU//fSTypcvr6ZNm0q6PcNfYmKiSpUqJX9//1Tj1a1bV5K0d+9eB6QDAAAAAAAACg4K+wAAAAAAAACTWeWU7c0RChUqpLlz58rLy0tdunTRQw89pGeffVZhYWEKDQ1V7dq19fPPP6tIkSKSpBMnTkhSmkV9kuTh4SFvb2/FxMTo2rVrDskIAAAAAAAAFASFzQ4AAAAAAAAAwDEiIyMVHByc5r79+/dnaowmTZpo/fr16tixo7Zv367t27dLkooWLaqWLVuqbNmytr5xcXGSJHd393TH8/DwUGxsrOLi4lS0aNHMngoAAAAAAABQoDFjHwAAAAAAAGAiw7Ao2QGbYVgckmfRokV6+OGHFRAQoN9++01xcXH666+/1LVrV7399ttq2bKlbt68+f+z314A2GJJ/7lT+gAAAAAAAADIPGbsAwAAAAAAAEyW7KDf31asWDHTM/Ol5fDhw+rRo4fKlCmjH374QR4eHpKkypUra8aMGTp79qyWL1+uOXPmqHfv3rYZ+OLj49Md8/r165IkT0/Pe84FAAAAAAAAFDTM2AcAAAAAAABAkrR48WLdvHlTjz32mK2o707PPPOMJGndunWSpICAAEnSqVOn0hwvPj5esbGx8vb2ZhleAAAAAAAAIAso7AMAAAAAAABMZjWcsr05QkqBXrFixdLcn9J++fJlSVLVqlXl4uKiixcvplnct3PnTklSSEiIQ/IBAAAAAAAABQWFfQAAAAAAAICJDFmULKdsb4Ys2c7i6+srSdq+fXua+7dt2yZJCgoKkiS5ubmpefPmkqQlS5ak6p/S9vjjj2c7GwAAAAAAAFCQUNgHAAAAAAAAQJL05JNPSpI2bNigadOm2e379ddfNX78eElS586dbe1Dhw6VJL399ts6fPiwrX3r1q2aMWOGihUrpl69euV0dAAAAAAAACBfobAPAAAAAAAAMFmyYcn25gh169bVsGHDJEl9+/bVgw8+qGeeeUahoaFq3Lix4uPj1bt3b7Vs2dJ2TMuWLTVo0CBFR0erdu3a6tChg9q2baumTZvq5s2bmj17tnx8fBySDwAAAAAAACgoCpsdAAAAAAAAACjIDElWB/z+1sh+FEnShx9+qEaNGmn69OnasWOH/vzzTxUtWlRhYWF68cUX9a9//SvVMRMmTFDt2rU1efJkrVq1Ss7OzmrRooVGjRql0NBQByUDAAAAAAAACg4K+wAAAAAAAADY6dixozp27JilYyIiIhQREZEzgQAAAAAAAIAChsI+AAAAAAAAwGTJRvZn7AMAAAAAAACQf1DYBwAAAAAAAJjIkEVWWRwyDgAAAAAAAID8gZ8CAwAAAAAAAAAAAAAAAACQh+S7wr5hw4apZ8+e2R4nOTlZvXr1UsmSJWWxWLRs2bJMHRceHq6IiIgM+4wZM0ZBQUHZzpgfzJ8/Xw899JAMwzA7CgAAAAAAgGmSDadsbwAAAAAAAADyjxz/xC8iIkIWi0Vt27ZNtW/Hjh2yWCyyWCyKioqytQ8ePFgPP/yw3N3dZbFkfgmRI0eOaOrUqRozZoytbcmSJWrQoIF8fHzk6uqqSpUqadSoUUpKSspwrK+//loLFizQsmXLdPbsWbVp0ybTOcywfv16hYaGqlSpUnJxcVFgYKAGDBig2NhYu37r1q1TWFiYvL295eXlpSZNmmj16tV3HX/u3LmqXbu2PDw8VLZsWf373/9WYmKibX9MTIw6dOigokWLqkaNGlq/fr3d8Tt37lRQUFCqPM8995yuXbumBQsW3PO5AwAAAAAA3M8MSclyyvbGzyYBAAAAAACA/CNXfsobEBCgn3/+2a54T5KmT5+uwMDAVP2Tk5P1r3/9S3379s3S80ycOFHNmze3G9PHx0fDhw/Xxo0bdejQIX344Yf69NNPNWzYsAzH+uuvv+Tn56fQ0FD5+vrKxcUlS1lym6enp1555RWtXr1af/31lz799FP99NNP6t69u63P8ePH1bZtW1WvXl2//vqrfvvtNwUHB6tdu3Y6duxYumPPnj1bL7/8svr3768//vhD8+fP13fffafevXvb+rzzzjuKiYnRjh071L17d3Xp0sU2C19SUpIiIiI0ZcoUeXt7243t5OSkXr166eOPP3bsBQEAAAAAAAAAAAAAAACA+1SuFPaVL19e4eHhmjlzpq3t6tWrWrRokV566aVU/SdNmqRBgwbpwQcfzPRzWK1WLVy4UE899ZRde/PmzdW5c2cFBwcrKChIHTt2VLdu3bR27dp0xwoPD9d//vMfnThxQhaLRa6urpKkW7duafTo0QoMDFSRIkVUpUoVTZkyJcNciYmJeuWVV+Tl5aXixYurb9++djPdSdKpU6fUqVMnlSxZUm5ubqpQoYI+/PDDTJ+7JNWrV0/dunVTSEiIAgMD1bp1a/Xr10/r1q2z9dm+fbsSEhL04Ycfqlq1aqpWrZo++OADJSYmaufOnemOPXv2bD333HN68cUXVaFCBbVs2VLvv/++FixYYCvWPHjwoLp27aoqVapo0KBBOn/+vC5duiRJeuutt1S/fn21a9cuzfE7deqkPXv2aN++fVk6ZwAAAAAAgHzBkKyGJdsbU/YBAAAAAAAA+UeuFPZJ0ssvv6zZs2fr1q1bkqTPPvtM5cuXV+PGjR0y/h9//KGYmBg1bNgww34HDx7UihUr1KxZs3T7LF26VP/+97/l7++vs2fP6vjx45Kk0aNHa+LEifrvf/+r/fv3a+DAgRo6dKhmz56d7lgjR47UV199pblz5+rXX3+Vu7t7qmLAvn37KiYmRj///LMOHjyo2bNny9/fPwtnn9qJEye0ZMkShYeH29rq168vV1dXTZ06VUlJSUpKStK0adPk7e2tRo0apTvWjRs35ObmZtfm7u4uwzC0YcMGSVKtWrW0atUq3bx5Uz/88IP8/f1VsmRJ7dixQwsXLtT48ePTHb9ChQoqXbp0hsWWKYKDg9PcIiMj73osAAAAAABA3mRxyFK8ksXsEwEAAAAAAADgILlW2NehQwdZrVZ99913kqQZM2bo5Zdfdtj4KUvJplcQ5+npKRcXF9WoUUPh4eEZFpr5+PjI09NThQoVkq+vr8qUKaOEhASNGzdOY8aMUbdu3VS5cmX1799fr7zyit566600x4mPj9e0adM0duxYdezYUVWrVtVHH32kKlWq2PWLiopSeHi46tatq6CgIIWHh6tr1673dB2Cg4Pl6uqqwMBA+fj4aPHixbZ9gYGBWrdunaZOnSo3Nze5ubnpk08+0S+//CI/P790x2zbtq0WLFigdevWyWq1KioqSm+++aYk6fTp05KkV199VR4eHqpUqZLGjRunJUuW6ObNm+rZs6emTZumTZs2qXbt2qpRo4ZmzZqV6jnKlSuno0eP3tM5AwAAAAAAAAAAAAAAAEB+Uji3nsjZ2VkvvPCCPv30U/n5+enIkSN67rnntHv3boeMn5CQIEm2ZXP/affu3UpISND27ds1cuRIlSlTxlaclhlHjhxRYmKi3Qx4khQWFqaJEyfq2rVrKlq0qN2+yMhIJSYmppoNLzQ0VMuWLbM9HjJkiF5++WX98MMPCg8PV7t27dS0adNMZ7vTjz/+qPj4eO3bt0+vvfaaXnnlFc2bN0+SdPHiRfXq1UutW7e2LYE8Y8YMPfHEE/r1118VEBCQ5pijRo3S5cuX1bp1ayUnJ8vNzU1vvPGGfv31VxUqVEiSVLRoUc2fP9/uuNdee00PP/yw6tWrpypVqmjDhg3y9vZWnTp19Mgjjyg4ONjW19XV1fYaZmT//v1ptgcHB+vYyei7XyAAAAAAAIA8xpBkNbL/+1tW4gUAAAAAAADyj1wr7JOkl156SZUrV1ZSUpK6dOkib29vh41dunRpSVJ0dLR8fX1T7a9UqZIkqWbNmnJyctILL7yg4cOHy8PDI1PjG0bWPxpNOcZiyXgZlJ49e+qxxx7TypUrtWbNGrVp00YdOnTQwoULs/ycgYGBkqQaNWqoTJkyCg8P16uvvqpq1app0qRJSkxM1KeffmrrX79+fVWuXFnTpk3Tu+++m+aYRYoU0eTJkzVx4kSdO3dOJUuW1JEjRzR8+HDbdf2n7du3a/Hixdq9e7fWrVunSpUqqWbNmpKk8PBwrVmzxq6w7/LlyypVqlSWzxcAAAAAACA/SGYZXQAAAAAAAAB3yLWleCWpfPnyatmypdauXevQZXglqW7dunJyctK+ffsy1d8wDN28eTPT41euXFkuLi5at26dXfv69esVGBiYarY+6XYxYZEiRbR582a79i1btqTq6+fnp549e2rBggWaNWuWPv/8c8XExGQ6X0YSExMlSdevX5eTk/1LbrFY5OTkJKvVetdxChUqpHLlysnFxUULFixQsWLF1LJlyzSfLyIiQjNmzFCxYsVktVrtrnVSUpLd8yUkJCgyMlL169e/11MEAAAAAAAAAGTgXPw5Hbp8SOfiz5kdBQAAAAAAZEKuztgnSUuXLlVCQoJKlCiRbp8jR44oLi5OJ06ckCTbcr0BAQHy8fFJ85jixYurYcOGWrt2rV2x2dixY/Xwww+rQoUKMgxDv//+u0aMGKEnn3wySzMGurm5aciQIRozZoxKly6tBg0a6KefftK0adM0ZcqUNI/x8PBQnz59NHr0aPn5+al69eqaNWuWDh06ZHce/fv3V7t27VSlShXduHFDS5cuVdmyZbOU75NPPlFQUJCqVq0qZ2dn7d27VyNGjFDdunUVEhIiSWrfvr3Gjx9vW/rXMAxNnz5dR48eVYcOHWxjtWjRQg0aNLDN4Hf06FGtW7dOjRo1UmJior744gt9+OGHmjVrlooVK5Yqy5gxY9S4cWO1atVKktSgQQP9+eefWrp0qXx8fLR69Wq7ZZA3bdokZ2dnNWvWLNPnCwAAAAAAkF8YsjhoKV5m/QOQ2k9RP2n+/vnae2mvrS2kVIi61+iu1kGtTUwGAEDec/nGZUUnRKuEWwn5uKb9vTQAAEBuyfXCPnd3d7m7u2fY58UXX9T69ettj+vUqSNJmjNnjiIiItI9rm/fvnr11Vc1duxY28x0CQkJGjBggE6ePKnChQsrKChIQ4YM0cCBA7OcfezYsSpUqJBGjBih8+fPKzAwUB9++KFefPHFdI957733dOPGDfXo0UOS1KVLF/Xr10+LFi2y9TEMQ4MGDdLJkyfl5uamhx9+WCtWrLAt4Tt37lz17NlTx44dU1BQUJrPYxiGXn/9dR07dkyS9MADD6hTp04aNmyYbZymTZvq66+/1vvvv685c+ZIur1k7zfffKOGDRvaxoqMjNQDDzxgN/a0adM0cOBAWa1W1alTR99++60ef/zxVDm2bdumr776Srt27bK1lS1bVjNnztSgQYOUmJiosWPH2l5TSZo/f77+9a9/pVkkCAAAAAAAUBCwFC+AnPDxjo81Z9+cVO17L+7VsPXDdCD6gIbUG2JCMgAA8pajV45q/PbxWn9qvQwZcrI4qWm5phpSf4gqeFUwOx4AACigLIZhGGaHcJTk5GTVqVNHI0aMULdu3cyO4zBvvPGGvv76a+3Zs0eFC+d6LWaOOn78uEJCQrR7926VL1/+nscJDg7WsZPRqvPoMAemw/3EARMb4D7lcvmW2RFgIueYG2ZHgImcjp0yOwJMZPH2MjsCTGJcizM7AkxkJN00OwJMsDnuG0lSXHKMyUlyRnBwsC4lntDg75pke6wJ7TeqpEuA9u/f74BkyIuCg4Mlidc4n8jp1/OnqJ80bP3dPy/9KOwjZu4DAORpOf2eeeDSAXX5oUua+4oVKaYFbRdQ3AcAwH0oP3yOkq9KYQoVKqQ5c+bo5s389UH/999/r8mTJ+e7oj5JioqK0qxZs7JV1AcAAAAAAHC/sxpO2d4A4E7zD8zPVL8FBxbkcBIAAPK2ybsnp7vvatJVjd8+PhfTAAAA/C3fVYrVq1dP9erVMzuGQ+3cudPsCDkmLCzM7AgAAAAAAACmMiQlO6AwL98sywEg287Fn9Pei3sz1XfPxT06F39Ovh6+OZwKAIC85/KNy9p0elOGfTac3qCYGzEq7lo8l1IBAADcxk95AQAAAAAAAADIR2ITY3O0PwAA+UV0QrSMu/xExmpYdSnhUi4lAgAA+Fu+m7EPAAAAAAAAuL9YZJXFIeMAgCR5u3jnaH8AAPKLEm4l5GRxktWwptvHyeKkkm4lczEVAADAbczYBwAAAAAAAJgoZSne7G4sxQsgha+Hr0JKhWSqb61StViGFwBQYPm4+qhx2cYZ9mlarinL8AIAAFNQ2AcAAAAAAAAAQD7TvUb3TPV7vsbzOZwEAIC8bdhDw1SsSLE09xUrUkxD6g/J5UQAAAC3UdgHAAAAAAAAmMmQrIYl2xtT9gG4U+ug1nrhwRcy7PPCgy+odVDrXEoEAEDeVMGrgha0XaBw/3A5WW5/fe5kcVK4f7gWtF2gCl4VTE4IAAAKqsJmBwAAAAAAAAAKMkNSsgN+f0tdH4B/GlJviGqUqKH5B+Zr78W9tvZapWrp+RrPU9QHAMD/V8Grgia1mKSYGzG6lHBJJd1KsvwuAAAwHYV9AAAAAAAAAO5Lly5dUvHixVWoUCGzozhUcHBwmu2RkZGqWLFiLqfB/a51UGu1Dmqtc/HnFJsYK28Xb/l6+JodCwCAVPLCPVBx1+IU9AEAgDyDpXgBAAAAAAAAU2V/GV6rYZFkMftEHG779u166623dODAAbv27777Tn5+fipTpoxKliypyZMnm5QQkqQrp6Sze2//L/IsXw9fVfOpRlEfAAAAAAD3CWbsAwAAAAAAAExm5fe3aZo0aZIWL16svn372tqOHz+uZ555RklJSfLz89P58+c1aNAg1apVS02aNDExrePs378/zfb0ZrExzf5vpC2TpdPb/27zf0hq2E8K7mheLgAAcF+6b+6BAABIS/wlKe6C5Fla8ihpdhrkE3xiCAAAAAAAACBP+vXXX1W7dm2VLPn3B+KzZs1SUlKSxo0bp9OnT2vbtm0qVKiQxo8fb2LSAmjVG9JXEfZFfZJ0atvt9lWjzUgFAAAAAEDuuviX9Pmz0oeVpGkNpY8q33588S+zkyEfoLAPAAAAAAAAMJEhKdmwZHszzD6RHHD+/HkFBATYtf3888/y9PRUv379JEl16tRRaGiodu/ebULCAmr/N9LmiRn32Tzhdj8AAAAAAPKri39Js1pKf62QUj6ZMay3H89qRXEfso3CPgAAAAAAAMBkVsOS7S0/Sk5OtnucmJio3bt3q3HjxipSpIitvWzZsjp37lxuxyu4tk7JZL+pOZsDAAAAAAAzrXpDunEl7X03Ym/vB7KBwj4AAAAAAAAAeVJgYKD++OMP2+NffvlFSUlJatGihV2/q1evysvLK7fjFUxXTt1ebjczTv1+uz8AAAAAAPlN/CXpr5UZ9zn8kxQfnTt5kC9R2AcAAAAAAACYyiKr4ZTtTcp/s/a1b99ehw8f1pAhQ/Tdd99p+PDhcnJy0pNPPmnXb9euXQoMDDQpZQFz/XLO9gcAAAAA4H4Qd0G25XfTY1iluPO5Egf5E4V9AAAAAAAAgIkMScmyZHu7y0fJ96Vhw4apQoUKmjhxojp27KiDBw9q8ODBqly5sq3Pb7/9ptOnT6tp06YmJi1A3H1ytj8AAAAAAPcDz9KS5S5lVxYnybNM7uRBvlTY7AAAAAAAAAAAkBYfHx/t3r1bS5Ys0YULF1SvXj01b97crs+5c+c0aNAgPffccyalLGC8/CX/hzK3HK9/g9v9AQAAAADIbzxKSpVbS3+tSL9P5daSR4ncy4R8h8I+AAAAAAAAwGRWI/8to+soHh4e6tGjR7r7n3zyyVRL8yKHNewnfRWRiX59czwKAAAAAACmafWWdGKrdCM29T5X79v7gWxgKV4AAAAAAAAAQOYFd5QaD864T+PBt/sBAAAAAJBflaoi9VolVWnz97K8Fqfbj3utur0fyAZm7AMAAAAAAABMZBiS1cj+728NwwFhTLZhw4ZsHd+0aVMHJcFdtXpTKltb2jrFflle/wa3Z+qjqA8AAAAAUBCUqiL9a7EUHy3FnZc8y7D8LhyGwj4AAAAAAADAZFaxFK8khYeHy2K592uRnJzswDS4q+COt7crp6TrlyV3H8nL3+xUAAAAAADkPo8SFPTB4SjsAwAAAAAAAJAndO/ePVuFfTCJlz8FfQAAAAAAAA5GYR8AAAAAAABgKouSDUcUs93/BXFz5841OwIAmOZMbIJiriepuHsRlfV2MzsOAAAAAMBkFPYBAAAAAAAAJjIkWQ0nh4wDALj//LD3rP638ah2n4y1tdUJ8NaLoRXULsTPvGAAAABAARQdl6hLcUkq6VlEJTxdzI6DAo7CPgAAAAAAAAAAABO8u+KgZqw/mqp914lY9ft8p/44XVEj21QzIRkAAABQsBy5EKf3VhzU6kMXZBiSk0VqXq20RraprkqlPc2OhwKKwj4AAAAAAADAZFaHLMV7/5s/f362ju/evbuDkgBAzvth79k0i/ruNH19pGqW82LmPgAAACAHHbkQp6embtbVG7dsbVZD+uXgBf1+7LKW9m1McR9MQWEfAAAAAAAAYDKrKOyTpIiICFksWb8WhmHIYrFQ2AfgvjJzU8ZFfSlmbTpKYR8AAACQg95bcdCuqO9OV2/c0nsrDmpmj4dyORVAYR8AAAAAAACAPOKNN964p8I+ALjfnIlN0K4TsZnqu/NErM7EJqist1vOhgIAAAAKoOi4RK0+dCHDPmsOXdDl+CT5eBTJpVTAbRT2AQAAAAAAACYy5JileI3sRzHdmDFjzI4AALki5npSlvtT2AcAAAA43qW4JBl3+VDFakgXryVS2IdcR2EfAAAAAAAAYCqLrIaTQ8YBANwfirtn7QvBrPYHAAAAkDklPYvIyXK7eC89ThapVFGX3AsF/H+O+MQQAAAAAAAAAHLU5cuXtWrVKi1atEhbtmwxOw4AZEtZbzfVCfDOVN+6Ad7M1gcAAADkkBKeLmperXSGfZpXK81sfTAFhX0AAAAAAACAmYzbS/Fmd8sXa/Gm4fz58+rSpYvKlCmjxx57TM8995xmzpxp2z916lT5+Pho48aNJqYEgKx7MbRCpvr1ymQ/AAAAAPdmZJvq8nJzTnOfl5uzRrapnsuJgNso7AMAAAAAAABMZEiyypLtLT/W9V26dEmNGjXSV199pZCQEPXr10+GYX+mHTp00LVr17RkyRKTUgLAvWkX4qc+YRUz7NMnrKLahfjlUiIAAACgYKpU2lNfv9JILauXlpPldpuTRWpZvbS+fqWRKpX2NDcgCqzCZgcAAAAAAAAAgLSMHTtWx44d01tvvaVRo0ZJkiZPnmzXp2zZsqpevbo2bNhgRkQAyJaRbaqpZjkvzdx0VLtOxNra6wZ4q1doBYr6AAAAgFxSqbSnZvZ4SJfjk3TxWqJKFXVh+V2YjsI+AAAAAAAAwGRWw2J2hDzpu+++U/Xq1W1FfekJDAzUr7/+mkupAMCx2oX4qV2In87EJijmepKKuxdRWW83s2MBAAAABZKPRxEK+pBnUNgHAAAAAAAAmIzCvrSdPXtWTz755F37ubq66tq1a7mQCAByTllvNwr6AAAAAAA2TmYHAAAAAAAAAIC0eHl56fTp03ftd/jwYfn6+uZCIgAAAAAAACB3UNgHAAAAAAAAmMiQRVYj+5uh/DfrX6NGjfT7779r//796fbZvHmz9u7dq6ZNm+ZiMgAAAAAAACBnUdgHAAAAAAAAmMwRhX350b///W8lJyerffv2Wr16taxWq93+TZs26fnnn1fhwoU1ZMgQk1ICAAAAAAAAjkdhHwAAAAAAAIA8KTQ0VOPHj9fx48f16KOPysfHRxaLRUuXLlWpUqUUFhamEydOaMKECapTp47ZcQEAAAAAAACHobAPAAAAAAAAMJlVlmxv+dXAgQO1adMmPfHEE7JarTIMQ1evXlVcXJweffRRrV27Vn379jU7JgAAAAAAAOBQhc0OAAAAAAAAABRkhuSQpXSN7EfJsx555BEtW7ZMhmEoOjpaycnJKlmypAoVKmR2NAAAAAAAACBHUNgHAAAAAAAA4L5gsVhUsmRJs2MAAAAAAAAAOY6leAEAAAAAAAAzGbdn7Mvulh+n7Nu/f7/eeust7dq1K90+u3bt0ltvvaWDBw/mYjIAAAAAAAAgZ1HYBwAAAAAAAJjMIYV9+dDEiRP1zjvvyNfXN90+vr6+evvttzVp0qRcTAYAAAAAAADkLAr7AAAAAAAAAORJ69evV506deTn55duHz8/P9WtW1dr167NxWQAAAAAAABAzqKwDwAAAAAAADCRoezP1mc1LDKU/2btO3XqlMqXL3/XfkFBQTp16lQuJAIAAAAAAAByR2GzAwAAAAAAAAAFnZFPl9LNriJFiujatWt37RcXFyeLhWsIAAAAAACA/IMZ+wAAAAAAAADkScHBwdq4caNiY2PT7RMTE6ONGzeqevXquRcMAAAAAAAAyGEU9gEAAAAAAAAms8qS7S0/+te//qVr167p6aef1tmzZ1PtP3v2rLp06aK4uDh169bNhIQAAAAAAABAzmApXgAAAAAAAMBEhiSrA5biNbIfJc/p3bu3Fi9erNWrV6ty5cpq27atKlasKIvFoiNHjujHH3/U9evX1ahRI73yyitmxwUAAAAAAAAchsI+AAAAAAAAAHlS4cKFtXLlSg0cOFDz5s3TkiVL7PYXKlRIPXv21MSJE+Xs7GxSSgAAAAAAAMDxWIoXAAAAAAAAMJlhWLK9Odq5c+c0ZMgQValSRW5ubvLx8VG9evU0fPjwNPvPnz9fDRo0kKenp3x8fNS2bVtt2bIl2znc3d01c+ZMnTx5UgsXLtR7772n9957TwsXLtSJEyc0a9YseXp6Zvt5AAAAAAAAgLyEGfsAAAAAAAAAMxmOWYrXkWvxbt26VW3btlVsbKxq1Kih9u3b69q1azpw4IA+/vhjffDBB3b9hw4dqvHjx8vNzU2PPvqobty4oVWrVunnn3/WV199pY4dO2Y7k6+vr7p27ZrtcQAAAAAAAID7AYV9AAAAAAAAAGzOnDmjtm3bKjExUUuXLk1VlPf777/bPV6zZo3Gjx+vEiVKaOvWrapcubKk28WB4eHh6tmzp8LDw1W8ePFcOwcAAAAAAADgfsdSvAAAAAAAAICpsr8M7+2leB2zHO/IkSMVGxurDz74IM2Z9ho0aGD3eNy4cZKkUaNG2Yr6JKlhw4bq06ePrly5otmzZzskGwAAAAAAAFBQMGMf8g2LITklOXDNGdxXrM6O+fIC958iF+LNjgATJRd1MTsCTGTx9zM7Akxkuca//wssK/f8APIfQ45ZitcR/4aMiYnRl19+KS8vL7344ot37X/jxg2tXr1aktS5c+dU+zt37qxPPvlEy5cv17///W8HJAQAAAAAAAAKBgr7AAAAAAAAAEiSNm/erMTERLVs2VLOzs5asmSJNm3apJs3b6patWp65plnVKZMGVv/Q4cOKTExUaVKlZK/v3+q8erWrStJ2rt3b66dAwAAAAAAAJAfUNgHAAAAAAAAmMzIIxOS7t+/X5JUpkwZNWnSRFu3brXb/+qrr2rOnDl6+umnJUknTpyQpDSL+iTJw8ND3t7eiomJ0bVr11S0aNEcTA8AAAAAAADkHxT2AQAAAAAAACazKvtL8UpSZGSkgoOD09yXUrSXkZiYGEnS/Pnz5eLiolmzZql9+/aKi4vTpEmT9PHHH+u5555T1apVFRISori4OEmSu7t7umN6eHgoNjZWcXFxFPYBAAAAAAAAmeRkdgAAAAAAAAAAeUNycrIk6datW/r444/1wgsvqGTJkgoKCtK4cePUuXNnJSUl6YMPPpAkGf9/qkGLJf3CRCMb0xE+9dRT6tev3z0fDwAAAAAAANyvmLEPAAAAAAAAMJlhOGbGvooVK2ZqZr70pMyo5+TkpB49eqTa/8ILL2jJkiVat26dXf/4+Ph0x7x+/bokydPTM8t5fvzxR3Xo0CHLxwEAAAAAAAD3Owr7AAAAAAAAABMZkqwOKOy793nx/hYUFCRJ8vX1lYuLS7r7L1y4IEkKCAiQJJ06dSrN8eLj4xUbGytvb+97Woa3fPnyGRYNAgAAAAAAAPkVS/ECAAAAAAAAkCTVqVNHkhQTE5PmErrR0dGS/p59r2rVqnJxcdHFixfTLO7buXOnJCkkJOSe8nTt2lXr16/XuXPn7ul4AAAAAAAA4H5FYR8AAAAAAABgJkMyHLA5Ysq+mjVrqnz58kpISNBvv/2Wan/KErx169aVJLm5ual58+aSpCVLlqTqn9L2+OOP31OeV199VU2aNFFYWJi++eYb3bx5857GAQAAAAAAAO43FPYBAAAAAAAAJjMMS7Y3RxkxYoQkaeDAgbp06ZKtfceOHRo3bpwkqU+fPrb2oUOHSpLefvttHT582Na+detWzZgxQ8WKFVOvXr3uKUvVqlW1f/9+HTlyRJ07d5abm5vKli2rChUqpNoqVqx4T88BAAAAAAAA5EWFzQ4AAAAAAAAAIO946aWXtHr1an311VeqWrWqGjVqpLi4OG3ZskVJSUl66aWX1LlzZ1v/li1batCgQZo4caJq166tVq1aKSkpSatWrZLVatXChQvl4+NzT1mioqLsHhuGwbK8AAAAAAAAKBAo7AMAAAAAAABM5agZ9xwza5+Tk5MWL16s8PBwzZw5U2vWrJHFYlH9+vXVp08fPf/886mOmTBhgmrXrq3Jkydr1apVcnZ2VosWLTRq1CiFhobecxar1ZqdUwEAAAAAAADuWxT2AQAAAAAAACazOnApXUdwcnJS37591bdv30wfExERoYiIiJwLBQAAAAAAABQgTmYHAAAAAAAAAAAAAAAAAAAAf6OwDwAAAAAAADCRIckwHLCZfSI5aMWKFerQoYPKlSsnFxcX9erVy27f0KFDdebMGRMTAgAAAAAAAI7FUrwAAAAAAACAyYw8thRvXtK3b1/NmDFDhmGoaNGiunnzpgzj7zJGb29vTZgwQf7+/ho6dKiJSQEAAAAAAADHYcY+AAAAAAAAAHnS7NmzNX36dDVo0EC7d+/WlStXUvVp2LChypUrp+XLl5uQEAAAAAAAAMgZzNgHAAAAAAAAmMlw0Ix9+XAt3hkzZsjHx0fff/+9SpQokW6/SpUq6ejRo7mYDAAAAAAAAMhZzNgHAAAAAAAAmMxwwJYf7d+/Xw0bNsywqE+SfH19deHChVxKBQAAAAAAAOQ8CvsAAAAAAAAA5ElOTk6yWq137XfmzBl5eHjkQiIAAAAAAAAgd7AULwAAAAAAAGAyhyzFmw9Vq1ZN27dv1/Xr1+Xu7p5mn+joaO3evVv16tXL5XQAAAAAAABAzmHGPgAAAAAAAMBsrMWbpm7duunixYvq16+fbt26lWq/YRgaOHCg4uLi9Pzzz5uQEAAAAAAAAMgZzNgHAAAAAAAAIE/q27evvv76a82bN0+bNm1S69atJUl79+7VsGHD9P333+uvv/5S8+bN1aNHD5PTAgAAAAAAAI7DjH0AAAAAAACAyQzDku0tP3J2dtbKlSvVp08fnThxQlOnTpUk7dy5Ux9//LEiIyPVq1cvLV++XE5OfNQJAAAAAACA/IMZ+wAAAAAAAAATGZIMByylm09X45W7u7umTp2qN998U+vXr1dUVJSSk5Pl7++vZs2aqWzZsmZHBAAAAAAAAByOwj4AAAAAAAAAeV6pUqXUuXNns2MAAAAAAAAAuYL1KQAAAAAAAABTZX8Z3ttL8ea/5XgrVKigESNG3LXfq6++qooVK+ZCIgAAAAAAACB3MGMfAAAAAAAAYCZDkuGAorx8uBZvVFSULl68eNd+ly5dUlRUVM4HAgAAAAAAAHIJM/YBAAAAAAAAuK/Fx8fL2dnZ7BgAAAAAAACAwzBjHwAAAAAAAGAyIx/OtpcbrFar/vzzT61du1YBAQFmxwEAAAAAAAAchhn7AAAAAAAAALMZDtjyiUKFCtk2SZo3b55d252bs7OzHnzwQZ0/f15du3Y1OTkAAAAAAADgOMzYBwAAAAAAACDPeOCBB2SxWCRJJ06ckLu7u0qWLJlm3yJFiqhs2bJq3769Bg4cmJsxAQAAAAAAgBxFYR8AAAAAAABgMsOwmB0hz4iKirL9s5OTk55++mnNnj3bvEAAAAAAAACACSjsAwAAAAAAAMyWj5bSdaS1a9fK19fX7BgAAAAAAABArqOwDwAAAAAAAECeFBYWlu6+q1ev6q+//pK/vz/FfwAAAAAAAMh3nMwOAAAAAAAAABR0hmHJ9pYf/fzzz3rhhRe0a9cuu/Zp06apTJkyevjhh+Xv769hw4aZlBAAAAAAAADIGRT2AQAAAAAAAGYyHLjlMzNnztQXX3yhSpUq2doOHDigAQMGKDk5WY888oiKFSum8ePHa/ny5SYmBQAAAAAAAByLwj4AAAAAAAAAedLOnTtVp04dFS1a1NY2Z84cGYahuXPnavPmzdq1a5dcXFw0depUE5MCAAAAAAAAjkVhHwAAAAAAAGA6iwO2/Of8+fPy9/e3a/vll1/k7e2tZ599VpIUGBiopk2bav/+/WZEBAAAAAAAAHIEhX0AAAAAAACA2ViGN02FCxdWUlKS7XFcXJz27dunJk2ayMnp7482S5UqpYsXL5oREQAAAAAAAMgRFPYBAAAAAAAAyJOCgoK0Y8cO2+MffvhBycnJatWqlV2/6OholShRIrfjAQAAAAAAADmGwj4AAAAAAADAbMzYl6Znn31WJ0+eVKdOnfTJJ5/o3//+t4oUKaIOHTrY+hiGoR07dqhChQrmBQUAAAAAAAAcrLDZAQAAAAAAAIACz7CYnSBPGjBggL777jt98803+uabb+Tk5KSPPvpI5cqVs/VZs2aNLl68qD59+piYFAAAAAAAAHAsCvsAAAAAAAAA5Enu7u7auHGjNm7cqAsXLqh27dqqXLmyXZ9ChQpp/PjxeuKJJ0xKCQAAAAAAADgehX0AAAAAAACAyYx8upSuIzg5OSksLCzd/eHh4QoPD8+9QAAAAAAAAEAuoLAPAAAAAAAAMBuFfQAAAAAAAADuQGEfAAAAAAAAgDzprbfeynRfi8Wi//znPzmYBgAAAAAAAMg9FPYBAAAAAAAAZjIstzdHjJPPjBkzRhaLRUY6axVbLLfP2TAMCvsAAAAAAACQr1DYBwAAAAAAAJjMwlK8aZozZ06a7VarVSdPntRPP/2krVu3ql+/fqpfv34upwMAAAAAAAByDoV9AAAAAAAAAPKkHj16ZLj/jTfe0Lvvvqt33nlHvXv3zqVUAAAAAAAAQM5zMjsAAAAAAAAAUOAZDtgKqFdffVX+/v567bXXzI4CAAAAAAAAOAyFfQAAAAAAAADuazVr1tSmTZvMjgEAAAAAAAA4DEvxAgAAAAAAAGYzLGYnuK9FRkbq1q1bZsdAHnAu/pxiE2Pl7eItXw9fs+MAAAAAAADcMwr7AAAAAAAAALMV4KV0syM2NlZjx47V7t271axZM7PjwEQ/Rf2k+fvna++lvba2kFIh6l6ju1oHtTYxGQAAuB9dvnFZ0QnRKuFWQj6uPmbHAQAABRSFfQAAAAAAAADypAoVKqS7Ly4uTtHR0TIMQ25ubnr33XdzMRnyko93fKw5++akat97ca+GrR+mA9EHNKTeEBOSAQCA+83RK0c1fvt4rT+1XoYMOVmc1LRcUw2pP0QVvNK/NwUAAMgJFPYBAAAAAAAAZmPGvjRFRUWlu8/Z2VkPPPCAwsLCNGLECNWoUSP3giHP+CnqpzSL+u40e99s1ShRg5n7AABAhg5cOqAuP3Sxa7MaVq07tU47LuzQZ20/o7gPAADkKgr7AAAAAAAAADMZckxhXz4sDrRarWZHQB43/8D8TPVbcGABhX0AACBDk3dPTnfftaRrGr99vCa1mJSLiQAAQEHnZHYAAAAAAAAAAACy6lz8Oe29uDdTffdc3KNz8edyOBEAALhfXb5xWZtOb8qwz4bTGxRzIyaXEgEAAFDYBwAAAAAAAJjPsGR/AwqY2MTYHO0PAAAKjuiEaBl3mQLbalh1KeFSLiUCAABgKV4AAAAAAADAdJZ8uIzuvdiwYUO2jm/atKmDkuB+4O3inaP9AQBAwVHCrYScLE6yGtZ0+zhZnFTSrWQupgIAAAVdvivsGzZsmKKjozVnzpxsjZOcnKzevXvr22+/VXR0tL755ht16NDhrseFh4crKChIc+fOTbfPmDFjNHfuXEVFRWUrY34wf/58TZo0Sb///rssFn5ZDgAAAAAAUJCFh4dn6zOi5ORkB6ZBXufr4auQUiGZWo63Vqla8vXwzYVUAADgfuTj6qPGZRtr4+mN6fZpWq6pirsWz8VUAACgoMvxpXgjIiJksVjUtm3bVPt27Nghi8Uii8ViK3I7ceKEXn75ZVWuXFlubm7y9/dXz549dfr06bs+15EjRzR16lSNGTPG1rZkyRI1aNBAPj4+cnV1VaVKlTRq1CglJSVlONbXX3+tBQsWaNmyZTp79qzatGmTpfPObevXr1doaKhKlSolFxcXBQYGasCAAYqNjbXrt27dOoWFhcnb21teXl5q0qSJVq9eneHYGzduVKdOneTv7y83NzdVrlxZY8aMUWJioq1PTEyMOnTooKJFi6pGjRpav3693Rg7d+5UUFBQqjzPPfecrl27pgULFmTr/AEAAAAAAO5rhgO2fKB79+7Z2lDwdK+Rudf9+RrP53ASAABwvxv20DAVK1IszX3FihTTkPpDcjkRAAAo6HJlxr6AgAD9/PPPioqKUlBQkK19+vTpCgwM1PHjx21tf/75p+Lj4zVhwgRVq1ZNZ8+e1eDBg/XYY49p9+7dKlSoULrPM3HiRDVv3lyBgYG2Nh8fHw0fPlzVq1eXh4eHdu3apZdffllXr17VJ598ku5Yf/31l/z8/BQaGpq9k88lnp6eeuWVV1SzZk15eXnp0KFDGjBggI4fP67vvvtOknT8+HG1bdtW3bt314wZMyRJEyZMULt27XTw4EGVL18+zbE3b96sihUratCgQXrggQe0c+dOvfzyyzp//rymTZsmSXrnnXcUExOjHTt2aOnSperSpYvOnj0ri8WipKQkRUREaMqUKfL29rYb28nJSb169dLHH3/Mh68AAAAAAAAFXEarYABpaR3UWgeiD2j2vtnp9nnhwRfUOqh1LqYCAAD3owpeFbSg7QKN3z5eG05vkNWwysnipKblmmpI/SGq4FXB7IgAAKCAyZXCvvLly6ty5cqaOXOm3n77bUnS1atXtWjRIr366qsaNWqUrW+rVq3UqlUr2+OKFStq6tSpevjhh3XgwAHVrFkzzeewWq1auHChPvroI7v25s2b2z0OCgrShg0b9Msvv6SbNzw83DbjnMVikYuLi27cuKFbt25p7Nixmjt3rs6ePaugoCANGjRI/fr1S3esxMREDR48WJ9//rmcnJzUtWtXeXl52fU5deqUBg0apPXr1ys+Pl5+fn565ZVX9H//93/pjvtP9erVU7169WyPAwMD1a9fP/3nP/+xtW3fvl0JCQn68MMPVbRoUUnSBx98oBkzZmjnzp3pFvaNHDnS7nH58uUVGRmp999/31bYd/DgQXXt2lVVqlTRoEGD9Oqrr+rSpUsqVaqU3nrrLdWvX1/t2rVLc/xOnTpp+PDh2rdvnx588MFMnzMAAAAAAAAADKk3RDVK1ND8A/PtluWtVaqWnq/xPEV9AAAg0yp4VdCkFpMUcyNGlxIuqaRbSZbfBQAApsmVwj5JevnllzVo0CCNGTNGhQsX1meffaby5curcePGdz02ZfnWkiVLptvnjz/+UExMjBo2bJjhWAcPHtSKFSv06KOPpttn6dKl+u9//6svvvhC27Ztk8VikSSNHj1aU6ZM0ZQpU9SgQQP99NNPGjp0qNzc3PTCCy+kOdbIkSP11Vdfae7cuapRo4b+97//acqUKfLx8bH16du3r+Li4vTzzz/Lx8dHUVFROnv2bIbncTcnTpzQkiVLFB4ebmurX7++XF1dNXXqVA0Zcnuq6GnTpsnb21uNGjXK0vixsbF2r0etWrW0atUq9erVSz/88IP8/f1VsmRJ7dixQwsXLtTu3bvTHatChQoqXbq01q5de9fCvuDg4DTbIyMjVcjZO0vnAAAAAAAAkFdY8slSuo6WmJio8+fPq3jx4rYfqv7TtWvXFBMTI19fXxUpUiSXE+aMjD4Dq1ixYi6nuT+0Dmqt1kGtdS7+nGITY+Xt4i1fD1+zYwEAgCzIS/dAxV2LU9AHAABM55RbT9ShQwdZrVbbsrAzZszQyy+/fNfj4uLiNGzYMD311FPy8/NLt9+xY8ckSf7+/mnu9/T0lIuLi2rUqKHw8HCNHz8+3bF8fHzk6empQoUKydfXV2XKlFFCQoLGjRunMWPGqFu3bqpcubL69++vV155RW+99Vaa48THx2vatGkaO3asOnbsqKpVq+qjjz5SlSpV7PpFRUUpPDxcdevWVVBQkMLDw9W1a9e7XZo0BQcHy9XVVYGBgfLx8dHixYtt+wIDA7Vu3TpNnTpVbm5ucnNz0yeffKJffvklw2v7TwcOHNAnn3yi/v3729peffVVeXh4qFKlSho3bpyWLFmimzdvqmfPnpo2bZo2bdqk2rVrq0aNGpo1a1aqMcuVK6ejR4/e0zkDAAAAAADc9wxL9rd86OOPP1b58uW1Z8+edPvs2bNH5cuX18SJE3MxGfIqXw9fVfOpRlEfAAAAAAC47+XajH3Ozs564YUX9Omnn8rPz09HjhzRc889l+FMbvHx8Wrfvr0KFy6cZjHYnRISEiRJrq6uae7fvXu3EhIStH37do0cOVJlypTRm2++men8R44cUWJiot0MeJIUFhamiRMn6tq1a6l+NRwZGanExMRUs+GFhoZq2bJltsdDhgzRyy+/rB9++EHh4eFq166dmjZtmulsd/rxxx8VHx+vffv26bXXXtMrr7yiefPmSZIuXryoXr16qXXr1nrppZck3S6wfOKJJ/Trr78qICDgruMfPnxYrVu31jPPPKMBAwbY2osWLar58+fb9X3ttdf08MMPq169eqpSpYo2bNggb29v1alTR4888ojdr25cXV1tr2FG9u/fn2Z7cHCwok5E3/V4AAAAAAAA3D+WLVum8uXLKzQ0NN0+oaGhCgoK0jfffKP/+7//y8V0OSejz8CADF05JV2/LLn7SF5p/wgeAIC8insgAAAAe7lW2CdJL730kipXrqykpCR16dJF3t7e6fa9cuWK2rVrp8TERP3yyy8Z9pWk0qVLS5Kio6Pl65v615iVKlWSJNWsWVNOTk564YUXNHz4cHl4eGQqu2FkfT2UlGNSlvJNT8+ePfXYY49p5cqVWrNmjdq0aaMOHTpo4cKFWX7OwMBASVKNGjVUpkwZhYeH69VXX1W1atU0adIkJSYm6tNPP7X1r1+/vipXrqxp06bp3XffzXDsffv2qVWrVnr88cc1Y8aMDPtu375dixcv1u7du7Vu3TpVqlRJNWvWlCSFh4drzZo1djfhly9fVqlSpbJ8vgAAAAAAAPc94/9vjhgnn4mMjEz1o9m0BAcH67fffsuFREAetf8bactk6fT2v9v8H5Ia9pOCO5qXCwAAAADuV/GXpLgLkmdpyaOk2WlQQOXaUrySVL58ebVs2VJr167NcBneixcvqlmzZjIMQ7/88ot8fHzuOnbdunXl5OSkffv2ZSqLYRi6efNmprNXrlxZLi4uWrdunV37+vXrFRgYmGq2Pul2MWGRIkW0efNmu/YtW7ak6uvn56eePXtqwYIFmjVrlj7//HPFxMRkOl9GEhMTJUnXr1+Xk5P9S26xWOTk5CSr1ZrhGL///rvCwsL09NNP69NPP001zj+fLyIiQjNmzFCxYsVktVrtrnVSUpLd8yUkJCgyMlL169e/l9MDAAAAAAC4/xkO2PKh+Pj4TP0w193dXVevXs2FREAetOoN6asI+6I+STq17Xb7qtFmpAIAAACA+9PFv6TPn5U+rCRNayh9VPn244t/mZ0MBVCuztgnSUuXLlVCQoJKlCiR5v4zZ86oRYsWcnNz0/z585WQkGBbotXLy0tubm5pHle8eHE1bNhQa9euVcuWLW3tY8eO1cMPP6wKFSrIMAz9/vvvGjFihJ588sm7zgJ4Jzc3Nw0ZMkRjxoxR6dKl1aBBA/3000+aNm2apkyZkuYxHh4e6tOnj0aPHi0/Pz9Vr15ds2bN0qFDh+yKFfv376927dqpSpUqunHjhpYuXaqyZctmKd8nn3yioKAgVa1aVc7Oztq7d69GjBihunXrKiQkRJLUvn17jR8/3rb0r2EYmj59uo4ePaoOHTrYxmrRooUaNGhgm8Fvw4YNateunZ5++mm99tprOn/+vK1vqVKlVKhQIbssY8aMUePGjdWqVStJUoMGDfTnn39q6dKl8vHx0erVq+2WQd60aZOcnZ3VrFmzTJ8vAAAAAAAA8r8HHnhA27dvv2u/HTt2yM/PLxcSAXnM/m+kzRMz7rN5glS2NjP3AQAAAMDdnNktfRpm32ZYpb9WSCe2Sr1WSaWqmBINBVOuztgn3f71bHpFfZL0888/69ChQ9q1a5cqVaokPz8/2/bFF19kOHbfvn312WefpZoNbsCAAQoJCVG9evX0/vvva8iQIfr888+znH3s2LHq37+/RowYoRo1amjChAn68MMP9eKLL6Z7zHvvvaeOHTuqR48eatCggWJjY9WvXz+7PoZhaNCgQXrwwQfVpEkTXbt2TStWrLAt4Tt37lxZLBZFRUWl+zyGYej1119XvXr1FBISoldffVWdOnXSqlWrbOM0bdpUX3/9tX799Vc98sgjatiwobZt26ZvvvlGDRs2tI0VGRmps2fP2h7Pnj1bcXFxmjNnjt3r4efnp5MnT9rl2LZtm7766it99NFHtrayZctq5syZGjRokJ555hmNHTtWderUse2fP3++/vWvf6lYsWIZXH0AAAAAAID8y2Jkf8uPHn30UR09elSTJk1Kt8+UKVMUGRmp1q1b52IyII/YmvaPzlP3m5qzOQAAAAAgP1j7dvr7bsTenjEdyEUWwzDyzcd+ycnJqlOnjkaMGKFu3bqZHcdh3njjDX399dfas2ePChfO9UkWc9Tx48cVEhKi3bt3q3z58vc8TnBwsKJORKtus387MB3uJ1Zni9kRYBLPw1fMjgATJRd1MTsCTFToWqLZEWAip2vxZkeASYyrcWZHgImMW7fMjgATbI77RpIUlxxjcpKcERwcrMPR0Xrg/4Zne6yTH36gyiVKaP/+/Q5IljecPHlSNWvW1LVr1/TEE0+od+/eqlixoiwWi44cOaJPP/1Uy5cvV9GiRbV7924FBQWZHTlHBQcHS1K+eo0Lsmy/nldOSeODM99/yH7Jy//engsAABNxDwQAyBXxl24vv6sMyqgsTtKwI5JH+hOaIe/ID/cQ+apKrFChQpozZ47++OMPs6M41Pfff6/Jkyfnu6I+SYqKitKsWbOyVdQHAAAAAACA/OmBBx7Qd999p86dO+u7777T8uXL7fYbhqGSJUvqyy+/zPdFfUAq1y9nvT+FfQAAAACQtrgLyrCoT7q9LG/ceQr7kGvyXaVYvXr1VK9ePbNjONTOnTvNjpBjwsLC7t4JAAAAAAAgv8s3a2o4XtOmTfXXX3/p008/1erVq3Xy5ElJt4v+WrZsqRdffFHFixc3OSVgAnefnO0PAAAAAAWJZ+nbM/IZ1vT7WJwkzzK5lwkFXr4r7AMAAAAAAADuNxYK+zLk7e2t4cOHa/jw7C9ZDOQbXv6S/0PSqW137+vfgNn6AAAAACAjHiWlSi2kw6vS71O5NbP1IVc5mR0AAAAAAAAAAADcg4b9Mtmvb87mAAAAAID84NH/Sq7eae9z9ZZavZWbaQBm7AMAAAAAAABMZUgyLI4ZJ5+LjY3VtWvXZBhpn2xAQEAuJwJMFtxROrNb2jwh/T6NB9/uBwAAAADIWKkqUq9V0qo3pMM/3V6W1+J0e6a+Vm/d3g/kIgr7AAAAAAAAALMVgKK8e3Xu3DmNGjVK3377rS5fvpxuP4vFolu3buViMiCPaPWmVLa2tHWK/bK8/g1uz9RHUR8AAAAAZF6pKtK/Fkvx0VLcecmzDMvvwjQU9gEAAAAAAADIk86ePauHHnpIZ86cUbly5VSqVClduHBBDRs21NGjR3X+/HlZLBY1bNhQzs7OZscFzBPc8fZ25ZR0/bLk7iN5+ZudCgAAAADuXx4lKOiD6ZzMDgAAAAAAAAAUZBZJFsMBm9knkgPefvttnTlzRm+99ZZOnjypNm3ayGKxaPPmzTp79qzWrVunatWqyWKxaMWKFWbHBczn5S/5hVDUBwAAAABAPkBhHwAAAAAAAGA2wwFbPrRy5UqVL19eo0aNSnN/06ZN9fPPP2vXrl0aO3ZsLqfLH87EJmj/mSs6E5tgdhQAAAAAAADcgcI+AAAAAAAAAOm6fPmySpcuLYvFomrVqmXYd/78+WrQoIE8PT3l4+Ojtm3basuWLff83KdPn1bt2rVtjwsVKiRJSkxMtLWVK1dOzZo105dffnnPz1MQ/bD3rDpM2axG761Ru082qdF7a9Rx6mb9sPes2dEAAAAAAMgzouMS9ee5a4qOS7x7Z8DBCpsdAAAAAAAAACjoLHl4xr2hQ4fq0qVLmeo3fvx4ubm56dFHH9WNGze0atUq/fzzz/rqq6/UsWPHLD93sWLFZBh/Xxxvb29Jtwv+KlSoYGt3dXXV6dOnszx+QfXuioOasf5oqvZdJ2LV7/Od+uN0RY1sk3ERJwAAAAAA+dmRC3F6b8VBrT50QYYhOVmk5tVKa2Sb6qpU2tPseCggmLEPAAAAAAAAMFseXYp39erVmjdvnl566aUM+61Zs0bjx49XiRIltGfPHi1btkwrV67Uhg0bVKhQIfXs2VMxMTFZfv6AgABFRUXZHj/44IOSpB9//NHWdv36dW3evFl+fn5ZHr8g+mHv2TSL+u40fX0kM/cBAAAAAAqsIxfi9NTUzfrl4O2iPkmyGtIvBy/oqambdeRCnLkBUWBQ2AcAAAAAAAAglYSEBPXp00c1atTQsGHDMuw7btw4SdKoUaNUuXJlW3vDhg3Vp08fXblyRbNnz85yhubNm2vfvn06f/68JKl9+/by8PDQsGHDNGLECE2aNEnNmjXT+fPn1aZNmyyPXxDN3JRxUV+KWZnsBwAAAABAfvPeioO6euNWmvuu3ril91YczOVEKKgo7AMAAAAAAADM5IjZ+nJg1r4333xTkZGRmjZtmpydndPtd+PGDa1evVqS1Llz51T7U9qWL1+e5QzdunVTp06ddPDg7Q/MfXx8NGPGDEnShx9+qMGDB2vbtm2qUaOG3nnnnSyPX9CciU3QrhOxmeq780SszsQm5GwgAAAAAADymOi4RK0+dCHDPmsOXdDl+KRcSoSCrLDZAQAAAAAAAICCzpJDS+neq71792rcuHHq2bOnmjZtarcc7j8dOnRIiYmJKlWqlPz9/VPtr1u3rm3MrKpVq5YWLVpk19a1a1c1btxYP/74o2JiYlSlShW1b98+w+JD3BZzPWtfOsRcT1JZb7ccSgMAAAAAQN5zKS7JtvxueqyGdPFaonw8iuROKBRYFPYBAAAAAAAAsLFarXrppZfk7e2tDz744K79T5w4IUlpFvVJkoeHh7y9vRUTE6Nr166paNGi2c4YEBCgPn36ZHucgqa4e9a+cMhqfwAAAAAA7nclPYvIyXK7eC89ThapVFGX3AuFAouleAEAAAAAAIB8IjIyUsHBwWlumTVp0iT9/vvv+vDDD1WiRIm79o+Li5Mkubu7p9vHw8PDrm9mNW/ePFPFhR999JGaN2+epbELorLebqoT4J2pvnUDvJmtDwAAAABQ4JTwdFHzaqUz7NO8Wmlm60OuoLAPAAAAAAAAMJvhgM0BTp48qVGjRiksLEwRERGZi/7/16exWCx37ZNV69at06FDh+7a788//9T69evv6TkKmhdDK2SqX69M9gMAAAAAIL8Z2aa6vNyc09zn5easkW2q53IiFFQsxQsAAAAAAADkExUrVtT+/fvv+fi+ffsqKSlJ06ZNy/QxKUvrxsfHp9vn+vXrkiRPT897zpaRGzduqHBhPurMjHYhfvrjdEVNXx+Zbp8+YRXVLsQvF1MBAAAAAJB3VCrtqa9faaT3VhzUmkMXZDVuL7/bvFppjWxTXZVK58znG8A/8WkXAAAAAAAAYDKLg2bcy67vv/9e3t7eeuWVV+zab9y4IUk6ceKEwsPDbX09PT0VEBAgSTp16lSaY8bHxys2Nlbe3t62IkBHunr1qrZs2SI/PwrRMmtkm2qqWc5LMzcd1a4Tsbb2ugHe6hVagaI+AAAAAECBV6m0p2b2eEiX45N08VqiShV1Yfld5DoK+wAAAAAAAACz5ZHCPkmKjY1Nd1nbhIQE275bt25JkqpWrSoXFxddvHhRp06dkr+/v90xO3fulCSFhIRk6vkrVLBfAnbJkiVat25dmn1v3bql8+fP69atW+rfv3+mxsdt7UL81C7ET2diExRzPUnF3YuorLeb2bEAAAAAAMhTfDyKUNAH01DYBwAAAAAAAECSZBhpVxhGRUWpfPnyqlq1qg4dOmS3z83NTc2bN9eKFSu0ZMkSDR482G7/kiVLJEmPP/54pjJERUXZ/tlisSguLk5xcXFp9nV2dlbZsmXVvn17vfvuu5kaH/bKertR0AcAAAAAAJAHOZkdAAAAAAAAACjwDAdsJho6dKgk6e2339bhw4dt7Vu3btWMGTNUrFgx9erVK1NjWa1W22YYhiIiIuza7twSExN17NgxTZw4Ue7u7jlybgAAAAAAAIAZmLEPAAAAAAAAMJMhWRxRmGdicV/Lli01aNAgTZw4UbVr11arVq2UlJSkVatWyWq1auHChfLx8cnyuHPmzFGlSpVyIDEAAAAAAACQt1HYBwAAAAAAACDbJkyYoNq1a2vy5MlatWqVnJ2d1aJFC40aNUqhoaH3NGaPHj0cnBIAAAAAAAC4P7AULwAAAAAAAGC2PL4Ub1BQkAzD0KFDhzLsFxERoe3btys+Pl6xsbFauXLlPRf13WnFihXq0KGDypUrJxcXF7tlfVesWKGhQ4fqzJkz2X4eAAAAAAAAIK9gxj4AAAAAAADAZA5Zijef6tu3r2bMmCHDMFS0aFHdvHlThvH3BfP29taECRPk7++voUOHmpgUAAAAAAAAcBxm7AMAAAAAAACQJ82ePVvTp09XgwYNtHv3bl25ciVVn4YNG6pcuXJavny5CQkBAAAAAACAnMGMfQAAAAAAAIDZmLEvTTNmzJCPj4++//57lShRIt1+lSpV0tGjR3MxGQAAAAAAAJCzmLEPAAAAAAAAMJvhgC0f2r9/vxo2bJhhUZ8k+fr66sKFC7mUCgAAAAAAAMh5FPYBAAAAAAAAyJOcnJxktVrv2u/MmTPy8PDIhUQAAAAAAABA7qCwDwAAAAAAADCRRZLFcMBm9onkgGrVqmn79u26fv16un2io6O1e/duhYSE5GIyAAAAAAAAIGdR2AcAAAAAAACYyRHL8ObT5Xi7deumixcvql+/frp161aq/YZhaODAgYqLi9Pzzz9vQkIAAAAAAAAgZxQ2OwAAAAAAAAAApKVv3776+uuvNW/ePG3atEmtW7eWJO3du1fDhg3T999/r7/++kvNmzdXjx49TE4LAAAAAAAAOA4z9gEAAAAAAABmY7a+NDk7O2vlypXq06ePTpw4oalTp0qSdu7cqY8//liRkZHq1auXli9fLicnPuoEAPw/9u47PIqq7eP4bxdIIYGEHnroSFNAekloUpVqRakqoEhVARFpj4ACAjZEBQEVH5UqYKGG3qsgNTQpoUNoSUhy3j94sw8hhZRNJiTfz3XtBZlzZuaeObuzZ3bvPQcAAAAA0g9G7AMAAAAAAAAsZkuniXnOkDVrVn355ZcaOXKk1qxZoxMnTigiIkKFChVSgwYNVKBAAatDBAAAAAAAAJyOxD4AAAAAAAAAaV6ePHnUoUMHq8MAAAAAAAAAUgWJfQAAAAAAAIDVGLEPAAAAAAAAwH1I7AMAAAAAAAAsxlS88VuzZo3WrFmjc+fOKTQ0NNY6NptN06dPT+XIAAAAAAAAgJRBYh8AAAAAAACANOnKlStq27at1q9fL2Piz34ksQ8AAAAAAADpCYl9AAAAAAAAgNUYsS9W/fv317p161S+fHm9/vrrKlGihDw8PKwOCwAAAAAAAEhxJPYBAAAAAAAAVjJyTmJfOkwOXLx4sQoVKqRNmzbJ09PT6nAAAAAAAACAVGO3OgAAAAAAAAAAiE1ERIRq1KhBUh8AAAAAAAAyHEbsAwAAAAAAACxmszqANKpKlSo6efKk1WEAAAAAAAAAqY4R+wAAAAAAAACrGSc80qFhw4Zp9+7dWrhwodWhAAAAAAAAAKmKEfsAAAAAAAAApEkNGzbUnDlz9Prrr+u3335T48aNVbBgQdlssY9xWL9+/VSOEAAAAAAAAEgZJPYBAAAAAAAAFrOl0xH3nOHmzZvKkiWLZs2apVmzZsVbNyIiIpWiAgAAAAAAAFIWiX0AAAAAAACA1Ujsi9XMmTPVvXt3GWNUpUoVFS9eXB4eHlaHBQAAAAAAAKQ4EvsAAAAAAAAApEkff/yxXF1d9fvvv8vf39/qcAAAAAAAAIBUQ2IfAAAAAAAAYDVG7IvViRMn5OfnR1IfAAAAAAAAMhwS+wAAAAAAAACL2Ujsi1XBggWtDgEAAAAAAACwhN3qAAAAAAAAAAAgNp06ddKmTZt07tw5q0MBAAAAAAAAUhWJfQAAAAAAAICVjBMf6cx7772nRo0aqVmzZlq7dq3V4QAAAAAAAACphql4AQAAAAAAAAvZ5JypeG3J30SaU6pUKUnSyZMn1aBBA7m4uMjHx0c2W8yjtdlsCgwMTO0QAQAAAAAAgBRBYh8AAAAAAACANOnEiRPR/g4NDdXJkyetCQYAAAAAAABIRST2AQAAAAAAAFZLh9PoOkNkZKTVIQAAAAAAAACWILEPAAAAAAAAsJgzpuIFAAAAAAAAkH6Q2Id0w7fAVa35+murw4BFmhZ4wuoQYBHGbsjYbFYHAEvZixW1OgRYKPzkv1aHAABILYZePwAAAAAAAICMh8Q+AAAAAAAAwGqM2AcAAAAAAADgPiT2AQAAAAAAAFYjsQ8AAAAAAADAfexWBwAAAAAAAAAAAAAAAAAAAP6HEfsAAAAAAAAAKxnJ5owR+xj1DwAAAAAAAEg3SOwDAAAAAAAArEZSHgAAAAAAAID7MBUvAAAAAAAAgDShePHiGjRokOPv2bNna+PGjRZGBAAAAAAAAFiDxD4AAAAAAADAYjZjkv1ID06cOKGLFy86/u7SpYu+/fZbCyMCAAAAAAAArMFUvAAAAAAAAIDV0kdeXrK5u7vr2rVrVocBAAAAAAAAWI7EPgAAAAAAAABpQtmyZbV8+XLNmjVLxYoVkyQFBQVp7dq1CVq/fv36KRkeAAAAAAAAkGpI7AMAAAAAAAAsZmPEPknS22+/rVdeeUXdunVzLPvrr7/0119/JWj9iIiIlAoNAAAAAAAASFUk9gEAAAAAAABIE1588UWVKlVKixcv1r///quZM2eqRIkSqlOnjtWhAQAAAAAAAKmKxD4AAAAAAADAaozY5/Dkk0/qySeflCTNnDlTdevW1YwZMyyOCgAAAAAAAEhdJPYBAAAAAAAAVjJOmoo3HSYHfvfddypZsqTVYQAAAAAAAACpjsQ+AAAAAAAAAGlS586drQ4BAAAAAAAAsITd6gAAAAAAAACADM844ZGO7du3T2+88YYqVqyoXLlyKXfu3KpYsaLefPNN7du3z+rwAAAAAAAAAKdjxD4AAAAAAADAQjY5ZypeW/I3kSZNmTJF77zzjiIiImTM/07UlStXtH//fn3zzTcaP368+vbta2GUAAAAAAAAgHMxYh8AAAAAAACANGn58uXq37+/XFxc1L9/f+3atUtXr17VtWvXtHv3bg0cOFCurq4aMGCAVq5caXW4AAAAAAAAgNMwYh8AAAAAAABgtXQ+lW5SffLJJ8qcObOWLVum2rVrRyurVKmSxo8fr3bt2ql+/fqaOHGiGjVqZFGkAAAAAAAAgHOR2AcAAAAAAABYzBlT8aZHW7dulZ+fX4ykvvvVqlVL/v7+2rJlSypGBgAAAAAAAKQspuIFAAAAAAAAkCbdvn1befLkeWi9PHny6Pbt26kQEQAAAAAAAJA6GLEPAAAAAAAAsJphyL7YFC5cWJs2bVJERIQyZcoUa53w8HBt2rRJhQsXTuXoAAAAAAAAgJTDiH0AAAAAAACAlcy9qXiT+1A6zA1s3bq1Tp48qVdffVXBwcExyoODg/Xaa6/p1KlTatOmTeoHCAAAAAAAAKQQRuwDAAAAAAAAkCYNGTJE8+fP1+zZs7Vw4UK1aNFCvr6+stlsOn78uJYuXarg4GAVL15cQ4YMsTpcAAAAAAAAwGlI7AMAAAAAAACslg5H23OGnDlzat26derRo4eWLl2qn376KUadli1batq0acqRI4cFEQIAAAAAAAApg8Q+AAAAAAAAwGK2SKsjSLsKFCigxYsX6/jx41q/fr3Onj3rWF63bl0VK1bM4ggBAAAAAAAA5yOxDwAAAAAAAECaV6xYMZL4AAAAAAAAkGGQ2AcAAAAAAABYjal4AQAAAAAAANyHxD4AAAAAAADAYjYS+wAAAAAAAADcx251AAAAAAAAAAAAAAAAAAAA4H8YsQ8AAAAAAACwkpFknDBkH6P+AQAAAAAAAOkGiX0AAAAAAACAxZiKFwAAAAAAAMD9mIoXAAAAAAAAAAAAAAAAAIA0hBH7AAAAAAAAAKsxYh8AAAAAAACA+5DYBwAAAAAAAFjIJudMxWtL/iYeCaGhoZozZ47++ecf2Ww2VahQQS+88IJcXFysDg0AAAAAAABwGhL7AAAAAAAAADwS9u7dq1atWun06dOOZTabTSNHjtSff/6pUqVKWRgdAAAAAAAA4Dx2qwMAAAAAAAAAMjxjkv9wgtu3b2vhwoXq3r27KlWqpOzZs8vDw0OPP/64Ro0apZs3b8a57uzZs1W9enV5enoqZ86catGihTZu3OiUuKL06NFDrq6uWr16tW7duqULFy5o4sSJOnnypPr06ePUfQEAAAAAAABWIrEPAAAAAAAAsJjNJP/hDHPmzFHbtm01Y8YMRUZGqlmzZqpXr56OHz+u4cOHq1q1arpw4UKM9QYMGKDOnTtr3759aty4sapXr67ly5erfv36WrBgQYL3f+DAgTjLwsLCtGXLFo0dO1Z+fn5yd3dX7ty51a9fPzVr1kxr165N0jEDAAAAAAAAaRGJfQAAAAAAAAAkSS4uLurVq5cOHz6sffv26ZdfftGff/6pQ4cOqXLlyjp48KD69esXbZ1Vq1Zp0qRJypUrl/bs2aOFCxfqzz//1Nq1a5UpUyZ17dpVV69eTdD+H3/8cb3zzjuxjgyYOXNmZcmSJdbEwgsXLsjd3T1JxwwAAAAAAACkRST2AQAAAAAAAFYyTnwkU6dOnfTll1+qVKlS0Zbnz59fX3zxhSRp/vz5CgsLc5RNnDhRkvT+++9HW69WrVrq2bOnrl+/rhkzZiRo/yNHjtRXX32lMmXKaM6cOdHK7Ha7nnnmGQ0ePFj/+c9/9Oeff2r+/Pnq0KGDtm/frrZt2ybpmAEAAAAAAIC0iMQ+AAAAAAAAwGJpZSre+Dz++OOSpNDQUF2+fFmSFBISopUrV0qSOnToEGOdqGWLFy9O0D6GDBmiAwcOqG7dunr55Zfl5+env//+21E+bdo0+fv764MPPlDLli3VoUMHR3LfpEmTknV8AAAAAAAAQFpCYh8AAAAAAACAhzp27JgkKUuWLMqZM6ck6eDBgwoNDVWePHlUqFChGOtUqVJFkrR3794E76dQoUL6+eeftXLlSl2+fFlVq1ZV3759df36deXMmVO//fabDh06pIULF2rRokU6evSofvnlF3l6ejrhKAEAAAAAAIC0gcQ+AAAAAAAAwFJGinTCwxlz8cZjypQpkqRmzZrJ1dVVknTq1ClJijWpT5I8PDzk7e2tq1ev6saNG4naX4MGDbRnzx6NGzdOs2bNUpkyZTRz5kxJUqlSpfT000/r6aefVvHixZN4RAAAAAAAAEDaRWIfAAAAAAAAYDXjhIekwMBAlS9fPtZHcvz++++aPn26smTJotGjRzuW37x5U5KUNWvWONf18PCIVjcxMmXKpAEDBujgwYN66qmn1L17d9WuXVu7du1K9LYAAAAAAACARwmJfQAAAAAAAADidODAAb388ssyxmj8+PF6/PHHHWXG3MsotNlsca4fVSexwsPDdenSJUmSj4+PZs+erbVr1+rOnTuqXr263njjDV29ejVJ2wYAAAAAAADSOhL7AAAAAAAAAIvZTPIfklSiRAnt378/1kdSnD59Ws2aNdPVq1c1YMAA9e3bN1p5tmzZJEm3bt2Kcxu3b9+WJHl6eiZon7/++qsqVaokd3d35cuXT56ennr++ecVGBioOnXqaMeOHZo8ebJ+/vlnlS5dWt98802Sjg0AAAAAAABIy0jsAwAAAAAAAKxkJBnjhIdzw7p06ZKaNGmiU6dOqWvXrpowYUKMOkWKFJF0LwEwNrdu3dK1a9fk7e3tSAKMz9SpU/XCCy/owoUL6t69u95991099dRTWrBggWrUqKEzZ87IbrfrzTff1OHDh9W6dWv17NlT1atX19atW5N3wAAAAAAAAEAaQmIfAAAAAAAAgGhu3Lih5s2b6+DBg2rXrp2++eabWKfbLVOmjFxdXXXx4sVYk/t27twpSapUqVKC9jthwgQVLlxYBw4c0FdffaWxY8dq/vz5+vXXX3XlyhXNmDHDUTdXrlz69ttvtXnzZtlsNtWuXTuJRwsAAAAAAACkPST2AQAAAAAAABZz1lS8zhAaGqrWrVtr+/btatq0qX766SdlypQp1rru7u5q2LChJGnu3LkxyqOWtWrVKkH7PnPmjKpWraocOXJEWx61j7Nnz8ZYp1q1atqyZYumTZuWoH0AAAAAAAAAj4LMVgcAAAAAAAAAZHhOnkY3qSIiIvTiiy9q9erVqlevnubPny8XF5d41xkwYID++OMP/ec//1HLli1VqlQpSdKmTZs0bdo0Zc+eXd27d0/Q/suXL6+VK1dq27ZtqlatmiTJGKOJEyfKZrOpXLlyca6b0H08CsqXLx/r8sDAQJUoUSKVowEAAEgd9IEAAACiI7EPAAAAAAAAgCTp888/14IFCyRJuXPn1htvvBFrvQkTJih37tySpMaNG6tv376aMmWKnnjiCTVp0kRhYWFavny5IiMj9eOPPypnzpwJ2v+ECRPUokUL1axZU2XKlFGOHDl07NgxnT9/XpUqVUpXyXsAACCBrp+Wbl+RsuaUvApZHQ0AAACQakjsAwAAAAAAACxmM2ljyL6rV686/h+V4BebESNGOBL7JGny5Ml64okn9Pnnn2v58uXKkiWLGjVqpPfff19169ZN8P4bNGigAwcOaMKECdq9e7euXr2qJ554Qi1atFCPHj0eOnpgerF///5Yl8c1ig0AAOnS/gXSxs+lM9v/t6xQNanWm1L5ttbFhRRDHwgAkO7cuiTdvCB55pU8cj+8PvAAEvsAAAAAAAAAq0VaHcA9I0aM0IgRI5K0bpcuXdSlS5dkx+Dr66vPP/882dsBAACPsOUfSBumxFx+epv0axfp7G6pycjUjgoAACBhLh6+1585/KckI9nsUqmmUpNRUp7SKbZbY4xMGvnxaFqQJUsWSVJkZNI+eLPZbLLZbM4MKdFI7AMAAAAAAAAAAACQNuxfEHtS3/02TJYKPMHIfQAAIG0JuyWNKRBzuYmUDv8hndoodV/h9OS+kJAQnT17VmFhYST23ef999+XJB06dChJ69tsNrm4uKhAgQJyc3NzZmgJZrdkrwAAAAAAAAAkSTZzbyre5D+sPhIAAAAn2PRFAut9mbJxAAAAOFvI9Xsj+TlzkyEhOnXqlEJDQ0nqe0Dx4sVVvHjxJK9vjFFoaKhOnTqlkJAQJ0aWcIzYBwAAAAAAAFiNz10BAACk66fvTbebEKe33qvvVShlYwIAAEioW5cfXufIX/fqeeRyyi7Pnj2riIgIubm5qWDBgsqcmVSwKPv375cklSlTJknrh4eH68yZM44REZOTJJhUtCYAAAAAAAAAAAAA692+kvj6JPYBAIC04tbFh9cxkdLN805J7DPGKCwsTJJUsGBBubi4JHub6YnNZpMk2e1Jm9DWxcVFBQsWVGBgoGOa46htphYS+wAAAAAAAACrMVUKAACAlDVnytYHAABISR55Hl7HZpc88zlld8YYx/S7jNSXMqLOa9S5Tu3EvqSlJAIAAAAAAABwGptJ/gMAAOCR51VIKlQtYXULVWe0PgAAkLYkZBS+Uk2dNg0v0r8Mmdj39ttvq2vXrsneTkREhLp3767cuXPLZrNp4cKFCVrP399fXbp0ibfOiBEj5Ovrm+wY07pu3bpp4MCBVocBAAAAAAAAAACAtKDWmwms90bKxgEAAJBYLh7Sm9skN+/Yy928pSajUjOiR8bQoUPl4+Mjm82myZMnP7R+ly5d5O/vH2+dhQsXqmLFis4J0CJpIrGvS5custlsatGiRYyyHTt2yGazyWaz6cSJE5KkyMhItW7dWr6+vnJ3d1fu3Ln11FNPaePGjQ/d19GjR/Xll19qxIgRjmVr165V69atVbRoUdlstmhl8Zk3b56+//57LVy4UOfOnVPz5s0TtJ5VFi1apBYtWsjHx0dZs2ZV+fLlNWXKFMewnHGJjIzUmDFjVLp0abm7u6tgwYLq1q2bLly44KizZ88ePfnkk/L09FSjRo3077//RtvGp59+qpYtW8bY9ogRIzR16lQdPXrUOQcJAAAAAADwKDIm+Q8AAID0oHxbqU6/+OvU6XevHgAAQFqTp7TUfblUuvm9aXele/+Wbn5veZ7S1sYXj4s3QvX5qiN6ZfoWtf1yg16ZvkVfrD6qizdCU3S/W7du1ZgxY/TFF1/o3Llzev3111NsXxcuXNBLL70kb29veXp6qlWrVjp27FiK7S+50kRinyQVKVJEy5YtcyTvRfnqq69UtGjRGPX9/Pz0448/6sCBA1q1apUKFiyop556KkZC2YOmTJmihg0bRtvmzZs3Va5cOX388cfy8fFJcMyHDx9W/vz5VbduXfn4+MjV1TXB61ohICBAtWrV0vz587Vv3z4NHDhQgwcP1scffxzvehMmTNCYMWM0evRoHThwQHPmzNHmzZv18ssvO+p0795d9evX1+7du5UjR45oo/AFBgbq448/1tdffx1j20WKFFGDBg30+eefO+9AAQAAAAAAHiVGskUm/yFy+wAAQHrRZKT07MyY0/IWqn5veZORVkQFAACQMHlKSy/9V3r7qNRr071/X/pvmk3qC7kboSHz96r2uJWasOyw1h25pF2nrmndkUsa/9ch1R63UkPm/62QuxEpsv/Dhw9Lktq1a+cYrCwlREZGqlWrVtq/f7+WLl2qNWvWKCQkRI0bN9bt27dTZJ/JlWYS+4oVKyZ/f399++23jmXBwcH66aef9Nprr0Wra7fbNWDAANWpU0e+vr6qVKmSJk+erFu3bmnr1q1x7iMyMlI//vij2rVrF215ixYtNHbsWD3//PMJTs7z9/fXsGHDdOrUKdlsNrm5uUmSwsPDNXz4cBUtWlQuLi4qXbq0vvjii3i3FRoaql69esnLy0s5cuTQG2+8odDQ6Nmup0+fVvv27ZU7d265u7urePHiGj9+fIJijTJp0iQNGzZMtWvXVvHixdWtWzd17dpVv/zyS7zrrVu3Tk2bNtXzzz8vX19f+fn5qWfPntq0aZOjzoEDB/T666+rZMmS6t69u/bv3y9JMsaoa9euGjNmjAoWLBjr9tu3b68ff/zxoSMHAgAAAAAAAAAAIIMo31Z6dYXUf7/UY929f19dzkh9AADg0eGRS8pX7t6/aVTI3Qh1nrFVP239V3cjYs/buRth9NPWU+o8Y6vTk/u6dOmiV155RdK9fDCbzaagoCB99tlnKlWqlFxcXFS0aFGNHDlSERFx79sYo2HDhilv3rzy9PTUCy+8oODg4Gh1Vq5cqW3btun7779XnTp1VLVqVf388886ffq0/vvf/zr1uJwlzST2SVKPHj00Y8YMhYeHS5J++OEHFStWTHXq1Il3vTt37ujLL7+Uh4eHqlevHme9v//+W1evXlWtWrWSHev8+fM1cOBAFSpUSOfOndPJkyclScOHD9eUKVM0ZswY7d+/X3369NGAAQM0Y8aMOLc1ePBg/frrr5o5c6Y2b96srFmzxkgGfOONN3T16lUtW7ZMBw4c0IwZM1SoUKFkH8e1a9eUO3fueOvUq1dP69at044dOyRJZ8+e1a+//hptat3HH39cf/zxhyIjI/XHH3+ocuXKku5Nwevt7a1OnTrFuf2aNWvq0qVL2rdvX7KPBwAAAAAA4NHjhGl4jRFD9gEAgHTJq5CUv9K9fwEAAOBUIxfv15bjVxJUd8vxKxq5+B+n7n/KlCmaPHmyJOncuXM6d+6cFi1apLffflt9+/bV/v37NWbMGE2aNEkjR8Y9avOnn36qiRMnaty4cdq5c6eqVKmir776KlqdDRs2qFChQqpUqZJjWa5cuVSjRg2tW7fOqcflLJmtDuB+bdq00VtvvaXffvtN7dq107Rp09SjR484648ZM0ZjxozRrVu3VKhQIa1cuVKFCxeOs/7x48clySkJcTlz5pSnp6cyZcrkmL73zp07jidJx44dJUmlSpXS0aNHNWrUKHXr1i3Gdm7duqWpU6dq0qRJatv23i+MJkyYoICAAF26dMlR78SJE+rQoYOqVKkiSfL19U32MaxevVq//PKL5s+fH2+9d955R+Hh4apRo4ZsNpvCw8P19NNPa9asWY46M2bM0BtvvKFJkyY5XhxHjx7VhAkTHHNhz5o1S15eXvrss89Uo0YNx7pR7XHs2DFVrFgx3ljKly8f6/LAwECViDljMwAAAAAAwKOBnDwAAAAAAACkogs3QjR3x+lErTN3x78a0KS08mRL2IyoD+Pl5SUvLy9JcuRfjRkzRr169VLv3r0l3cu9unjxooYMGaL33nvPMavq/caPH6+33nrLkZv17rvvasWKFVq+fLmjzrlz55Q/f/4Y6/r4+Ojs2bNOOR5nS1Mj9mXJkkXdunXT119/rU2bNuno0aN6+eWX46zfs2dP7dq1S2vXrlWdOnX07LPP6tSpU3HWv3PnjiTF2sDOcPToUYWGhsrf3z/acj8/P508eVI3btyIsU5gYKBCQ0NVu3btaMvr1q0b7e/+/fvrP//5j2rUqKFBgwZp7dq1yYp18+bNatu2rYYNG6Znnnkm3roLFizQp59+qq+//lo7d+7UkiVLdPTo0WiJimXLltWqVat06tQpLVy4UHnz5lXXrl01btw47dq1S7Nnz9aWLVs0fPhwdejQQWFhYY51o9ojqn0AAAAAAAAAAAAAAAAApJxftsU9/W5c7kYY/bL93xSKSLpx44ZOnToVa+5VSEiIjhw5EmOd4OBgnTlzJkbuVdRso1GMiftYbTZb0oNOQWkqsU+SXnvtNa1YsUJDhw7V888/L29v7zjr5syZU6VKlVK9evX0008/KUuWLPryyy/jrJ83b15J0uXLl50dtqT4nwAPW+dhT5CuXbvq5MmT6tmzp86ePavmzZs7RgVMrICAADVp0kQDBgzQ8OHDH1q/X79+6t27t7p166aKFSuqZcuWmjZtmubMmaNDhw7Fus6UKVOUK1cudezYUStWrNDTTz8tb29vtWzZUnfu3Im23pUr94b0zJMnz0Nj2b9/f6yPEiVKJPDoAQAAAAAA0h6bMcl+AAAAAAAAAAmV0Cl4H7T5WMrkXUkPz72KLb8qoblXBQoU0Llz52IsDwoKinUkv7QgzSX2FStWTI0bN9bq1avjnYY3NsYYhYaGxllepUoV2e127du3L7lhxqpUqVJydXVVQEBAtOVr1qxR0aJFlS1bthjrlCxZUi4uLtqwYUO05Rs3boxRN3/+/Oratau+//57TZ8+XXPmzNHVq1cTFeOSJUvUokULDR8+XB988EGC1rl9+7YyZcoUbVnU35GRkTHqHzlyRJ988ommTZvmqHP37l1J99ooPDw82np///237Ha7Y5phAAAAAACADMeY5D8AAAAAAACABLoZGp6q6yVE9uzZVbhw4Vhzr9zc3FSyZMkY63h5ealgwYIxcq927doV7e86dero9OnT0fLGrly5oi1btqhevXrOOwgnymx1ALGZP3++7ty5o1y5csVavmnTJm3btk316tVTrly5dO7cOX3++ec6ffp0vKPY5ciRQ7Vq1dLq1avVuHFjx/KbN2/q6NGjkqSwsDAFBQVp9+7dcnFxUbly5RIct7u7u/r3768RI0Yob968ql69uv766y9NnTpVX3zxRazreHh4qGfPnho+fLjy58+vxx57TNOnT9fBgweVM2dOR73evXurZcuWKl26tEJCQjR//nwVKFAg3hENH/TLL7/o5Zdf1uDBg/Xyyy8rKCjIURY1T/WZM2fUqFEjjR07Vm3btpUktW3bVhMnTlTJkiVVrVo1nTx5Uv369VPFihVVunTpaPuIjIxU165d9fHHHytfvnySpPr16+utt97SK6+8ovXr18vNzU1lypRxrBMQEKBatWopR44cCT4WAAAAAAAAAAAAAAAAAEnj6Zq0tLGkrpdQQ4cOVZ8+fVSqVCk1bdpUW7du1YgRIzRw4EC5ubnFus7A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d+7cxmazmZIlS5oTJ06Yxx57zGTNmtV4eHiYbNmymU2bNlkdJgAAAAAglVy5csV4eXkZSWbevHmO5UFBQaZkyZJGklm1alWCtnX37l1To0YNs2TJEhMREeFYHhERYYYOHWokmWzZspmLFy86/TjSk2HDhhlJplatWubGjRuO5RMnTjSSTP369RO8rVdeecVIMu3atTN37951LH/rrbeMJNOpUyenxp4ROau9xo4da9577z1z+vTpaMsPHz5sihQpYiSZIUOGODX2jMiZr6/7ffvtt0aSef31140k07RpU2eFnKE5s712795tXFxcTK5cucy6deuilUVERJht27Y5LW4gPs7sexljTP/+/Y0k07ZtWxMSEuJYHhwcbOrWrWskmQ8++MCpx4CEcXZbG2PMc889ZySZbt26mVu3bkUrO3v2LN/tWSQl2jrKnTt3TKlSpUz58uVN7dq1jSS+x7MI987pS0a97yaxDwAeETabzdjt9hiPqOVR/yL9adasmWnbtq3Zu3ev6d27tylXrpxp06aNCQ0NNWFhYeb55583TZo0sTpMpLDQ0FDj6upq9u7da3UosEjXrl1NcHBwjOU3b940Xbt2tSAipJZbt26ZnTt3Oj7ovXnzpvn666/NV199ZY4fP25tcEhxv/76q1m4cGGM5QsXLjRz5861ICIAQFrw8ccfG0mmdevWMcrmz59vJJlWrVolez+RkZGmbNmyRpKZOXNmsreXXoWFhRlvb28jyezcuTNGeaVKlYwks3379odu69y5c8Zut5ssWbKYoKCgaGUhISEmT548JlOmTDHKkHDObK/4zJkzx0gyvr6+ydpORpdS7XX+/HmTI0cO07hxY7N69WoS+5zE2e1Vr149I8ksXrzY2aECieLsvlfVqlWNJLN58+YYZQsWLDCSTPPmzZMTMpLI2W29cuVKI8lUq1YtWiIQrJeS91TvvfeesdlsZt26dcbPz4/EPgtx75x+ZOT77szOGPUPQOoYNWpUgut+8MEHKRgJrJA3b17VrFlT/fr1k81mkyQZY9S4cWN9++23KlasmMURIqVs27ZNAQEBqlChgsaNG6fs2bPr22+/lYuLiyRpyJAhaty4scVRIqW5uLgod+7cypQpk9WhwCKzZs3SuHHjlC1btmjL79y5o1mzZmnGjBkWRYaUtGvXLj311FO6fPmySpQooRUrVqh5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H/TUU09Jkry8vPTjjz/qyJEj2rZtm7744gv95z//SWbkSO/SWt/r1KlTatmypc6dO6fZs2c7Rnf6448/1L9/f9WtW1fLly9nmtYkSEttff91rFatWpo9e7ajrH379nJzc1OrVq306aef6r333mN68URKS20tSW+++aZCQ0M1derURK2H+KW1dn4Q986pJyPfd5PYBzyiAgMD1apVKx06dEg2my3GxYfEvvSvcOHC2rBhg27cuEFnPwOoWbNmtJv4MmXKaNasWRZGBKudP39eefLkkd1utzoUpJJWrVpFm6rh33//1fz581W+fHmmYk3HGLUVxYsX1549e/T5559r586dkqQePXrozTffVN68eS2ODilt586datq0qUJDQ3X79m3lyJFDly9fVtasWZU3b14S+4BH2IkTJ2JMKfgwt2/flnTvBx/3L4vtM4Goup6enkmK7/XXX9fSpUtVpkwZLV26lFGtHiKqTW7duhVreWLaw5nbQuxS8hzv3btXbdq0UVhYmD799FO1bds26YFCkvPaKzw8XK+99pq8vLw0YcIE5wYJh5S4HhYsWNCR1He/rl27atu2bQoICEhitMhI0lrfq3Pnztq3b58WLlwYbcr2Tp06ydPTU+3bt9eAAQO0cePGRMWMtNXW928vajSv+7Vs2VL58uXT+fPntXXrVj7jTaS01Nbz5s3Tb7/9pmHDhqls2bKJignxS0vtHBvunVNPRr7vJrEPeEQNGDBAxYsX15o1a1SiRAlt3bpVFy5c0IABA/T5559bHR5S2M6dO3Xs2DHZbDaVLFnSMWoP0j/aPmMLDw/X8OHD9dlnn+n27ds6fPiwihcvrnfffVfFihVTr169rA4RKah169Zq2bKl3nrrLd28eVM1a9bU7du3dePGDU2bNk3du3e3OkSkAEZthSTly5dPo0ePtjoMWOCdd95Ry5YtNX36dHl7e2vLli2y2+166aWXNGTIEKvDA5AM27dvT/K62bNnl5eXl65fv67Tp0+rXLlyMeqcPn1aklSkSJFEb/+dd97RjBkzVLhwYS1fvly5c+dOcqwZRdR5jjrvD0pMezhzW4hdSp3jwMBANW3aVNeuXdOIESP01ltvJS9QSHJee50+fVq7d++Wj4+Pnn322Whl165dkyRt3bpV/v7+8vT01JIlS5IZecbkzNeXr6+vJKlo0aLxll+4cCGR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8+apevXqcnd3V758+dSpUyedPXs2zvpxcfZ9kpT4e477Yzh8+LBeeOEF5cuXT3a7Pdo5+OWXX1StWjXHMXft2lXnz5+P1j+63/39nCj39yES0o4J8e2330qSOnbsmKj1HiUXL17U22+/rTJlysjNzU05cuRQ8+bNtXbt2hh17793TUyf5MH2GjFihOM7tfvvA+K6L46IiNDHH3+s0qVLy9XVVYULF9agQYPivM9NyuvuQXH1SRPbJ0mq+891UFCQXn31VRUqVEiZM2fW5MmTJcXdP5cSdr/zoIS8Z93vxRdf1KVLlxwznaRnjNgHPMJcXV3VuXNnde7c2epQAACp4P4O7K1btyRJHh4eVoWDVHb48GH997//jTa9Q5YsWTR48GCmYk7HFi1apAEDBmjv3r0xXu937txRlSpV9Omnn6pFixYWRYiUsmjRIg0ZMkSFCxeOUVaoUCENGjRIc+fOdXypjPTNz89Pfn5+VocBZBjr169Xy5YtFRwcrPLly+uZZ57RmTNnNH/+fP3+++9aunRprKMlHz9+XNWrV5e3t7f8/Px05MgR/fnnn9qxY4f27t2rnj17atWqVapVq5aKFy+u1atX680331SWLFn02muvxdheaGioGjZsqMDAQDVs2FBhYWFauXKlo28QlRySEJGRkWrTpo3Wrl0rPz8/VapUKUFTug8cOFCffPKJMmXKpFq1aqlQoUI6e/asVq9erWvXrqlly5aOut27d9etW7dUvnx5VaxYUcHBwdq6dauGDh2qlStXatmyZcqUKVOCY47N+fPn1bBhQ/3zzz8qWLCgmjRpotu3b2vTpk1q27atxo4dq8GDB8dYL7nnsmTJkurcubNmzZolDw8PdejQwVEW2ygK7777riNRr1mzZtq4caO+/vprHThwwPHFUZRbt26pefPmWrdunXLnzq26devKbrdr06ZNevXVV7Vt27ZkJfOHhobqpZdeUo4cOVSuXDlVqVJFly5d0qZNm/Tmm29q69atsX4Rk1R37tyRn5+fTp48KT8/P1WpUuWhP0KOiIjQCy+8oLlz58rV1VV169ZV7ty5derUKS1YsEAuLi6qU6dOtHWuXbumWrVq6fr166pevbpu3ryptWvXqnv37oqMjNSrr7760Fg7dOigEydOaN68ecqXL5+aNWsmSTFGnYjvmDZv3qw+ffqocOHCKl26tGrVqqVTp05p2bJlWrZsmb799tsY06bd3+aenp6qV6+esmXLpuPHj2vmzJkqWLCgKlSooGbNmik8PFwbNmzQ448/rieeeMKxjZIlSz70+Hr06KGvv/5abm5uatiwobJmzaqAgAC99957Wrx4sVauXCl3d/cY6y1evFhTpkxRhQoV1LRpU/3999/6/ffftWXLFu3bt08+Pj4P3ff9+vfvr6+++ko1a9ZUsWLFtGbNGg0aNEi3bt3SU089paeeekq+vr5q2LCh9uzZo0WLFmnfvn36+++/Y40vLl9++aUmTpyoJ598Us2aNdO2bdv03//+Vzt27NCePXsSta3YDBkyRB999JGyZcumunXrytvbW+fOndOePXt09OhRvfnmm/GuH3UdWb9+vQIDA9W0aVPHuYxqzxs3bqhBgwbasWOH8uTJo1atWunWrVtatWqV1q1bp82bNzu+SH2YRYsWqX379oqIiFDt2rVVpEgRbd26VTVq1NDTTz+dpHNw5coV1axZU6GhofL399fVq1e1evVqde7cWcePH9fw4cOj1X/nnXe0e/duVahQQQ0bNlRoaKh27typjz76SEuWLNHmzZvl6enpqL906VI988wzstlsqlOnjmrXrq2rV6/q8OHDGjdunHr06BGt/qeffqp+/frJbrerevXqKliwoPbt26fPPvtMS5Ys0YYNGxI1NVx875UnTpzQq6++qnz58qls2bKqXr26goKCtHHjRq1bt04HDx6MlgAVV1tLinYMUe+zbm5uql69unLnzq0dO3boP//5j/744w+tWbOGz/7SmMT0dx52HQ8PD1dQUJD++usvlShRQnXr1nWU3183qX3TEydOqFq1anJzc1PdunV1/vx5bdq0SW3atNEff/yhp556SpJUt27dBMURl7Fjx+q9995T5syZ5efnp9y5c2vDhg366KOPtGDBAq1du1b58uWLsd727dv15ptvqlChQmrUqJGOHj2qtWvXqlGjRtq2bZsqVKjw0H1L9xIo165dq4CAgGgJOVGJWCEhIdq8eXO05JiAgABlypQp2rEmtZ8Zm379+unTTz+Vq6urGjVqJHd3d33//ffauHGjMmeOOz0jLCxMTz31lPbv36/q1avrscce05o1azR48GDduHHDMWNbQvumSTmmzz//XG+99ZYyZcrkaM8VK1aoZs2aevzxxxN0/A9y5n1SUu85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U1gtX748SzI9y77Jqscyb968+vPPP5/696cnMDBQDg4OWr9+vbW7Q1azx755FjVr1lSRIkUUERHx1NOcGe3WrVtKSkpKc3quP//80/rl88MCAgLk4OCgH374QRcuXHjs70guQHua513evHlVp04dWSyWNKdgPnr0qCIjI+Xs7KwaNWo88f3mJskFJidPnky1LDQ09Invp06dOurUqZMk6ciRI8+U6ZVXXpGTk5P27dunU6dOpVr+zTffSJLq16//2ONV/fr1JUnfffddqmU3b958qm182KFDh9J8zJKfZ/Xq1bOOZXTs+e677564A5K7u7t1KrvkxzhPnjwKCAjQ9evXn+qi6Gdx48YNOTs7p9kNJr1jaUavXzc3N1WqVEmHDx9Ot5gT2U9m3lMfdxx/3HJ7nZtm5v1GkvWC4//973+pll29elWhoaEym83y8/N79pAZcHJyUu3atXXu3DlrocrD35n4+/trz5492rBhgyRZi/6SZeXjnLytK1asSLUsPDz8mabgfFRG56aZ2aaM3j9OnjypQ4cOPXXGrGSL89qM9ldMTIwOHjyYqfvNCsnnaU/bMS8rhYeHy2KxPNXt0ddXep7ldfc05yTpyexxLyP2OGZn5pzEnp72886zuH37ti5evKhy5coZcqGePVHYB+QgX331VYrbwoULU40l35C7nDlzRjExMTpz5kyGN1tdFQbjsO/xMAcHB02YMEE3btxQZGSkIiMjdePGDU2cODHFlwhRUVFZ+mEE2cO///1vxcXF6eDBg7px44Zu3rypiIgIXbt2TUOHDjU6HmwoMDBQ0dHRCgkJ0aRJk/Txxx9r9erViomJUePGjY2OBxtZs2aNfHx8lJCQkGpZQkKCfH199cMPPxiQDLa2ZcsWhYWFWW9btmxJcywsLMzoqECu1Lt3bxUvXlwTJ07UwoULUxUexMfHa/HixXbrtPH++++nuIjjzJkzGjdunCTpvffes+nvHjFihCRp8ODBioqKSrEsPj4+xXGoYsWKkqT58+eneMy2b9+uTz75JEvy1KlTR40bN9aWLVs0ZMiQVJ22kpKSFBoaqh07dqT581nxWJYqVUpXrlzRzZs3M7cRjyhdurS6du2qU6dOqVOnTml+0btr165nes9P3jePdnlesWKFFi9enOn7zSqOjo4aMmSILBaL/vWvf6XqinL9+nXt3LnToHQZ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3Jlli88cFSpU0MSJE/X++++rYcOGCgwMVLFixbRz506ZzWY1b95c69atM+xiiNdff12LFi3S3r17s+X58LPIkyePQkJC1LRpU40YMUKzZs1S9erVVahQIf366686fvy4bt68qZCQEL388sspfvZpzknS4+joqH/84x9at26datSoIV9fXzk6OqpevXrq1q1bprcrq44l6cnMOYm9Pc3nnWcRHh4uSc/F38fp2AfkUJ6enmlOu3fz5k0KOXKh2NjYJ74hd2HfIzOedDoT5Czx8fFydXVNNV6iRIkUV/Lj+RYZGan79+8bHQNZICoqKsOOjIGBgTp8+LAdE8EIDg4O+u2331KNX7t2LdWUfwCyTr169XTkyBENHTpUTk5OCgsLU2hoqG7fvq1mzZrp22+/VZUqVWyeI1++fAoLC1O7du20e/du/fTTTypTpoymTp1qnWLM1mbPnq2QkBA1btxYR48e1cqVK3XmzBk1btw4RcelihUrav/+/WrevLni4uK0du1a/f777/r888+zrGOf9Ne57549ezR9+nR5eXlp//79Wr16tc6fPy8fHx/NnTtXHTt2TPVzWfVYfvzxx+rfv7/+/PNPffvtt1qwYIG+//77Z9qmAgUKKDQ0VF9++aV8fX119OhRhYSEKDo6WuXLl9eUKVMUFBSU6ftv2LChduzYoUaNGikmJkbr16+Xo6OjVq5cmWUFL88qT548Wrlypb766ivVqlVL+/bt0+rVq3Xp0iW1bt36iTpuGGXkyJH6+uuvVb16de3cuVObNm1SjRo1tGfPHtWsWTPNn3F2dtaWLVs0Y8YMVapUSVu3btX69et18+ZNde/eXe+884513fz58+v777/X3//+dx06dEiLFi3SggUL0uwG+KjPP/9cixcvlo+Pj7Zu3ap169bJ1dVVEyZMUFhYmAoUKJBlj0NO4+TkpM2bN6tdu3a6c+eOfvjhByUlJenbb79N83VRvnx5TZ06VQEBAYqNjdWqVau0c+dOeXh46NNPP1V4eHiWfOHv7OysrVu3asyYMXJxcdHatWu1fft21axZU0uXLtWsWbOe+L5at26tjRs3qkGDBjp48KB+/PFHValSRbt371aFChUyla9YsWLavXu3mjRpoi1btig8PFxVqlTRwoULrR1QH153//79at++vR48eKC1a9fqwoULGjt2bJpTREt/vee0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", + "text/plain": [ + "" + ] + }, + "metadata": { + "image/png": { + "width": 900 + } + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "# Metaclusters transferred to all regions by each fold model\n", + "\n", + "Stage-1 refit agreement (held-out cells relabelled by the saved model): fold0 0.929; fold1 0.941; fold2 0.960\n", + "\n", + "Pairwise ARI of metacluster labels between models on cells both assign: fold0-fold1 0.63; fold0-fold2 0.97; fold1-fold2 0.70\n", + "\n", + "| Meta | folds | composition | model | share train / held-out | regions train (pos) / held-out (pos) | emp-base train / held-out | margin train / held-out |\n", + "|---|---|---|---|---|---|---|---|\n", + "| M1 | 3 | Tcell_CD4 24%, Epithelial 23%, Tcell_CD8 19%, Imm_other 13% | fold0 | 56.80% / 37.54% | 27 (16) / 14 (6) | -0.04 / +0.25 | -0.19 / -0.19 |\n", + "| | | | fold1 | 7.45% / 35.40% | 12 (10) / 12 (5) | +0.33 / -0.15 | -0.16 / -0.16 |\n", + "| | | | fold2 | 55.53% / 43.73% | 26 (11) / 15 (11) | +0.03 / +0.05 | -0.82 / -0.82 |\n", + "| M2 | 3 | Tcell_CD4 23%, Epithelial 22%, Imm_other 15%, Tcell_CD8 12% | fold0 | 21.98% / 37.24% | 26 (15) / 15 (5) | +0.06 / -0.18 | -0.18 / -0.18 |\n", + "| | | | fold1 | 58.32% / 35.64% | 32 (17) / 12 (5) | -0.09 / +0.04 | -0.19 / -0.18 |\n", + "| | | | fold2 | 31.81% / 41.63% | 28 (11) / 16 (11) | -0.08 / -0.08 | -0.81 / -0.82 |\n", + "| M3 | 1 | Epithelial 91%, Fibroblast 9%, Imm_other 0%, Tcell_CD4 0% | fold1 | 4.64% / 2.81% | 6 (6) / 2 (2) | +0.39 / +0.61 | -0.15 / -0.16 |\n", + "\n", + "## Consensus patient-level shares (mean over the models' assignments of every region)\n", + "\n", + "| Meta | folds | composition | share pos | share neg | present pos / neg | AUROC | p | q |\n", + "|---|---|---|---|---|---|---|---|--\n" + ] + } + ], + "source": [ + "display(Image(filename=f\"{WORK}/meta/match/metaclusters_overview.png\", width=900))\n", + "print(open(f\"{WORK}/meta/transfer/report_transfer.md\").read()[:1500])" + ] + }, + { + "cell_type": "markdown", + "id": "8f9f3874", + "metadata": {}, + "source": [ + "## 7. Classical spatial statistics\n", + "\n", + "The same cell cache feeds the model-free comparison: neighbourhood enrichment\n", + "on the cell graph, cellular neighbourhoods, contact fractions and cross-K,\n", + "each per tile, averaged per image and tested between classes. With three\n", + "images the patient-level tests cannot be significant; `min_per_group=1` lets\n", + "the tables fill for the demo.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "2ccf681c", + "metadata": { + "execution": { + "iopub.execute_input": "2026-10-13T18:20:09.168073Z", + "iopub.status.busy": "2026-10-13T18:20:09.168000Z", + "iopub.status.idle": "2026-10-13T18:20:12.242445Z", + "shell.execute_reply": "2026-10-13T18:20:12.241985Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "spatial stats: exit 0\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
metricn_posn_negmean_posmean_negdiffpqab
0Endothelial|Tcell_CD414151.178-0.2741.4520.0010.037EndothelialTcell_CD4
1Fibroblast|Myeloid_CD11c11120.357-0.5910.9470.0070.104FibroblastMyeloid_CD11c
2Tcell_CD4|Tcell_CD417222.4190.5691.8500.0120.114Tcell_CD4Tcell_CD4
3Myeloid_CD11c|Myeloid_CD6815110.974-0.4661.4400.0380.265Myeloid_CD11cMyeloid_CD68
4Myeloid_CD68|Tcell_CD414161.160-0.1011.2610.0530.272Myeloid_CD68Tcell_CD4
5Tcell_CD4|Tcell_CD815211.000-0.1431.1430.0580.272Tcell_CD4Tcell_CD8
6Fibroblast|Imm_other14191.2120.1151.0970.1130.452FibroblastImm_other
7Imm_other|Tcell_CD415220.015-0.8940.9090.1780.618Imm_otherTcell_CD4
\n", + "
" + ], + "text/plain": [ + " metric n_pos n_neg mean_pos mean_neg diff p \\\n", + "0 Endothelial|Tcell_CD4 14 15 1.178 -0.274 1.452 0.001 \n", + "1 Fibroblast|Myeloid_CD11c 11 12 0.357 -0.591 0.947 0.007 \n", + "2 Tcell_CD4|Tcell_CD4 17 22 2.419 0.569 1.850 0.012 \n", + "3 Myeloid_CD11c|Myeloid_CD68 15 11 0.974 -0.466 1.440 0.038 \n", + "4 Myeloid_CD68|Tcell_CD4 14 16 1.160 -0.101 1.261 0.053 \n", + "5 Tcell_CD4|Tcell_CD8 15 21 1.000 -0.143 1.143 0.058 \n", + "6 Fibroblast|Imm_other 14 19 1.212 0.115 1.097 0.113 \n", + "7 Imm_other|Tcell_CD4 15 22 0.015 -0.894 0.909 0.178 \n", + "\n", + " q a b \n", + "0 0.037 Endothelial Tcell_CD4 \n", + "1 0.104 Fibroblast Myeloid_CD11c \n", + "2 0.114 Tcell_CD4 Tcell_CD4 \n", + "3 0.265 Myeloid_CD11c Myeloid_CD68 \n", + "4 0.272 Myeloid_CD68 Tcell_CD4 \n", + "5 0.272 Tcell_CD4 Tcell_CD8 \n", + "6 0.452 Fibroblast Imm_other \n", + "7 0.618 Imm_other Tcell_CD4 " + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "run(\n", + " [\n", + " \"grass_mil.spatial_stats.cli\",\n", + " f\"cells_dir={WORK}/cells/fold1\",\n", + " f\"output_dir={WORK}/spatial\",\n", + " f\"labels.label_file={RAW}/tile_labels.csv\",\n", + " \"labels.id_column=patch_id\",\n", + " \"labels.label_column=epithelial_rich\",\n", + " \"labels.patient_column=patch_id\", # tiles as the test unit for the demo\n", + " \"reference=[Epithelial]\",\n", + " \"partners=[Myeloid_CD68,Myeloid_CD11c,Tcell_CD8,Tcell_CD4,Fibroblast]\",\n", + " \"infiltrating=[Myeloid_CD68,Tcell_CD8]\",\n", + " \"nhood.n_perms=100\",\n", + " \"cn.n_cn=4\",\n", + " \"cn.sample_size=5000\",\n", + " \"crossk.radii=[10,20,40]\",\n", + " \"min_per_group=3\",\n", + " \"positive_name=epithelial-rich\",\n", + " f\"hydra.run.dir={WORK}/spatial/hydra\",\n", + " ],\n", + " \"spatial stats\",\n", + ")\n", + "pd.read_csv(f\"{WORK}/spatial/nhood_tests.csv\").head(8).round(3)" + ] + }, + { + "cell_type": "markdown", + "id": "7834b38d", + "metadata": {}, + "source": [ + "## Where to go next\n", + "\n", + "Point the same commands at your cohort: whole cores or regions as units\n", + "(`data.sample_unit=full`, `nested_cv.id_kind=region`), a real patient column,\n", + "patient-grouped folds, the pretraining preset at its defaults\n", + "(`task.total_steps=200000`, 4-hop ego-graphs) and the regimes of the preprint\n", + "(`regime=linear_probehead|linear_ftall|linear_ftall_reg`). See\n", + "`docs/nested_cv.md` and `docs/niche_discovery.md`.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/vignettes/README.md b/vignettes/README.md index b7a9474..80264b8 100644 --- a/vignettes/README.md +++ b/vignettes/README.md @@ -7,13 +7,15 @@ before pointing the framework at your own cohort. | Vignette | What it covers | |---|---| | [`01_anndata_to_spatial_graphs.ipynb`](01_anndata_to_spatial_graphs.ipynb) | Turning a `squidpy` dataset into the manifest + per-sample files `grass-mil` expects, building the cellular graphs, and inspecting them | -| [`02_pretraining_and_niches.ipynb`](02_pretraining_and_niches.ipynb) | BGRL self-supervised pretraining, exporting per-instance embeddings, and discovering spatial niches with the interpretability report | +| [`02_pretraining_and_niches.ipynb`](02_pretraining_and_niches.ipynb) | GRASS-MIL pretraining (BGRL, `regime=pretrain_a3`), fine-tuning with `regime=mil`, exporting per-instance embeddings, and discovering spatial niches with the interpretability report | +| [`03_nested_cv_niches_and_metaclusters.ipynb`](03_nested_cv_niches_and_metaclusters.ipynb) | The preprint's analysis chain on the same tiles: nested cross-validation, cell extraction, niche attribution, cross-fold metaclusters, evaluation statistics and classical spatial statistics | -Run them in order: the second reads the graphs the first builds. +Run them in order: the second reads the graphs the first builds, the third +reuses the pretrained encoder of the second. ## Dataset -Both use `squidpy.datasets.mibitof()` — MIBI-TOF imaging of colorectal +All three use `squidpy.datasets.mibitof()` — MIBI-TOF imaging of colorectal carcinoma: 3 images, ~3,300 cells, 36 protein markers, 8 annotated cell types (Hartmann et al.). It is a natural fit because `grass-mil` is built for cell-resolved spatial proteomics: every cell has coordinates, a type, and a @@ -42,5 +44,5 @@ jupyter lab vignettes/ `squidpy` is used only to fetch the public dataset; it is not a runtime dependency of `grass-mil`. -Both notebooks write to `vignettes/vignette_work/`, which is git-ignored. Delete +The notebooks write to `vignettes/vignette_work/`, which is git-ignored. Delete it to start clean. diff --git a/vignettes/data/anndata/mibitof.h5ad b/vignettes/data/anndata/mibitof.h5ad deleted file mode 100644 index 4fd206b..0000000 Binary files a/vignettes/data/anndata/mibitof.h5ad and /dev/null differ