GOLDMARK — Manuscript Reference Pipeline - https://artificialintelligencepathology.org/
This repository is in support of the manuscript GOLDMARK: Governed Outcome-Linked Diagnostic Model Assessment Reference Kit and is composed of the end-to-end benchmarking pipeline:
- Target construction (TCGA via GDC download + variant labeling / OncoKB annotation)
- Tiling (20x/40x coordinate generation)
- Feature extraction (canonical pathology foundation models + custom encoders) with QC metadata
- Training (gated-attention MIL reference head) on predefined patient-level splits
- Inference + external inference and attention export (best checkpoint + fixed late epoch)
The design goal is reproducibility and clarity: cross-validation alone is not sufficient—the pipeline is built around reciprocal external testing (e.g., TCGA→external and external→TCGA) under identical preprocessing and evaluation criteria.
- Fig 1 (workflow): images/fig1_workflow.pdf
- Fig 2 (cross-validation + reciprocal external): images/fig2_cv_reciprocal_external.pdf
- Fig 3 (top eight task grid): images/fig3_top8_task_grid.pdf
Downloads and labels real TCGA SVS slides via GDC and runs:
download → tiling → features → training → inference
This is a real-data run (WSI deps + OpenSlide required). Expect multiple TB of downloads and non-trivial runtime.
Preflight checklist (avoid the common failure modes)
- Most encoders are hosted on Hugging Face; approval + a valid HF token are required to run this pipeline. Without access you will hit
401 Unauthorizedduring feature extraction. - Ensure
configs/secrets.envexists and is sourced for non-interactive runs (SLURM/nohup). Example:set -a; source configs/secrets.env; set +a - OncoKB is required for TCGA mutation labeling in this pipeline. Set
ONCOKB_TOKENinconfigs/secrets.env(or provide precomputed OncoKB labels). The pipeline will refuse to run without it. - If you use
h-optimus-0, yourHUGGINGFACE_HUB_TOKEN(orHF_TOKEN) must have explicit access tobioptimus/H-optimus-0. A401 Unauthorizedmeans the token is missing or lacks access. gdc-clientrequires newer glibc on some clusters. If it fails, GOLDMARK falls back to the GDC API (slower). For full cohorts, prefer a compatiblegdc-clientor a newer node.- Some clusters do not provide
python. Usepython3or setPYTHON_BINin the SLURM script. - The LUAD KRAS end-to-end run defaults to the full cohort (
--per-class 0). Set--per-classto a small value for a quick sanity check.
git clone https://github.com/chadvanderbilt/GOLDMARK.git
cd GOLDMARK
# Put tokens in one place (never commit the filled file).
# GOLDMARK commands will auto-load `configs/secrets.env` if present.
cp configs/secrets.env.example configs/secrets.env
# edit configs/secrets.env (GDC token file path, ONCOKB_TOKEN, HF_TOKEN, ...)
# Option A (recommended on HPC): conda
source /home/vanderbc/.bashrc
bash scripts/setup_conda_goldmark_env.sh
conda activate goldmark
# Option B (sudo/venv): installs system deps + creates venv
bash scripts/setup_goldmark_sudo.sh
# Installs gdc-client into bin/ (ignored by git)
python scripts/install_gdc_client.py --dest bin/gdc-client
# Note: some HPC nodes ship older glibc; if gdc-client fails to run, the pipeline
# will fall back to downloading via the GDC API (sufficient for small runs).
# Downloads 2 tumor + 2 normal slides (smallest by file size) and runs the full pipeline on CPU.
python scripts/gdc_smoke_test_tcga.py --project-id TCGA-COAD --per-class 2 --device cpu --forceOutputs land in runs/gdc_smoke_test/ (including runs/gdc_smoke_test/inference/inference/inference_results.csv).
This repo ships two end-to-end scripts that exercise the full mutation-label → MIL training → attention export pathway and validate reciprocal external inference (TCGA→external):
scripts/tcga_to_external_smoke_test.py(fast, minimal; good for “does anything run?”)scripts/tcga_cv_to_external_full_run.py(full run by default; 5-fold CV, attention exports, external inference)
What it does:
- Downloads TCGA-LUAD diagnostic slides via GDC (filters to
-00-DXunless--allow-non-dx) - Only downloads slides with targets: the script labels cases using MAFs, then builds a subset manifest and downloads only those labeled slides.
- Labels each patient as KRAS positive/negative from the GDC Masked Somatic Mutation MAF
- Builds 5 independent 70/30 splits with per-split
valassignments (train/val/test stored as columns) - Trains for a full cohort by default and writes a
cv_summary.csv - Runs held-out test inference per split and exports attention vectors
- Runs external inference on the external LUAD cohort using the best split checkpoint (and links it from all split dirs)
python scripts/tcga_cv_to_external_full_run.py \
--run-name TCGA-LUAD \
--per-class 0 \
--device cuda \
--epochs 25 \
--patience 50 \
--forceFor GitHub users, we provide generic launchers that accept options via environment variables. These are not hard-coded to a specific project or gene.
Make sure the goldmark conda environment is active (or set CONDA_ENV=goldmark before running the scripts).
SLURM (GPU example) — examples/slurm/submit_tcga_cv_to_external.sh
# Example: minimal resume (20x+40x, no external)
export PROJECT_ID=TCGA-LUAD
export GENE=EGFR
export ENCODER=h-optimus-0
export RUN_NAME=TCGA-LUAD
export RUN_MODE=resume
export PER_CLASS=0
export TARGET_MPP=0.5
export EXTRA_TARGET_MPP=0.25
sbatch examples/slurm/submit_tcga_cv_to_external.sh20x only (no external)
export PROJECT_ID=TCGA-LUAD
export GENE=EGFR
export ENCODER=h-optimus-0
export RUN_NAME=TCGA-LUAD
export RUN_MODE=resume
export PER_CLASS=0
export TARGET_MPP=0.5
export EXTRA_TARGET_MPP=
sbatch examples/slurm/submit_tcga_cv_to_external.sh40x only (no external)
export PROJECT_ID=TCGA-LUAD
export GENE=EGFR
export ENCODER=h-optimus-0
export RUN_NAME=TCGA-LUAD
export RUN_MODE=resume
export PER_CLASS=0
export TARGET_MPP=0.25
export EXTRA_TARGET_MPP=
sbatch examples/slurm/submit_tcga_cv_to_external.shExternal inference (20x+40x) — examples/slurm/submit_tcga_luad_EGFR_cv_to_external.sh
export PROJECT_ID=TCGA-LUAD
export GENE=EGFR
export ENCODER=h-optimus-0
export RUN_NAME=TCGA-LUAD
export RUN_MODE=resume
export PER_CLASS=0
export EXTERNAL_PER_CLASS=0
export TARGET_MPP=0.5
export EXTRA_TARGET_MPP=0.25
export EXTERNAL_MANIFEST=/path/to/external_manifest.csv
export EXTERNAL_ROOT=/path/to/foundation_model_training_images/EXTERNAL
sbatch examples/slurm/submit_tcga_luad_EGFR_cv_to_external.shNotes for SLURM users:
- You must set a GPU-capable partition in the script (
#SBATCH -p ...) and match your cluster’s GPU request syntax (e.g.,--gres=gpu:1). - Some clusters require different SBATCH fields (account/QoS/time/memory). Adjust the header in
examples/slurm/submit_tcga_cv_to_external.shto match local policy.
External inference only runs if EXTERNAL_MANIFEST (and optionally EXTERNAL_ROOT) are set.
Minimum required columns in EXTERNAL_MANIFEST:
- Label column: one of
<GENE>(e.g.,EGFR),label_index,label, ortarget - Slide ID column: one of
slide_id,DMP_ASSAY_ID,sample_id,case_id, orslide
Optional columns:
feature_path(absolute/relative path to features per slide; overridesEXTERNAL_ROOT)OncoTree_Code(used to resolve features underEXTERNAL_ROOT)scanned_slides_exist(rows with 0/false are skipped)
nohup (interactive node) — scripts/nohup_tcga_cv_to_external.sh
# Example: minimal resume (20x+40x, no external)
export PROJECT_ID=TCGA-LUAD
export GENE=EGFR
export ENCODER=h-optimus-0
export RUN_NAME=TCGA-LUAD
export RUN_MODE=resume
export PER_CLASS=0
export TARGET_MPP=0.5
export EXTRA_TARGET_MPP=0.25
bash scripts/nohup_tcga_cv_to_external.sh20x only (no external)
export PROJECT_ID=TCGA-LUAD
export GENE=EGFR
export ENCODER=h-optimus-0
export RUN_NAME=TCGA-LUAD
export RUN_MODE=resume
export PER_CLASS=0
export TARGET_MPP=0.5
export EXTRA_TARGET_MPP=
bash scripts/nohup_tcga_cv_to_external.sh40x only (no external)
export PROJECT_ID=TCGA-LUAD
export GENE=EGFR
export ENCODER=h-optimus-0
export RUN_NAME=TCGA-LUAD
export RUN_MODE=resume
export PER_CLASS=0
export TARGET_MPP=0.25
export EXTRA_TARGET_MPP=
bash scripts/nohup_tcga_cv_to_external.shExternal inference (20x+40x)
export PROJECT_ID=TCGA-LUAD
export GENE=EGFR
export ENCODER=h-optimus-0
export RUN_NAME=TCGA-LUAD
export RUN_MODE=resume
export PER_CLASS=0
export EXTERNAL_PER_CLASS=0
export TARGET_MPP=0.5
export EXTRA_TARGET_MPP=0.25
export EXTERNAL_MANIFEST=/path/to/external_manifest.csv
export EXTERNAL_ROOT=/path/to/foundation_model_training_images/EXTERNAL
bash scripts/nohup_tcga_cv_to_external.shWatch logs:
tail -f runs/<run-name>/logs/nohup.outEnd-to-end example (foreground) — examples/run_tcga_luad_egfr_end_to_end.sh
# Example: minimal force (20x+40x, no external)
export PROJECT_ID=TCGA-LUAD
export GENE=EGFR
export ENCODER=h-optimus-0
export RUN_NAME=TCGA-LUAD
export RUN_MODE=force
export PER_CLASS=0
export TARGET_MPP=0.5
export EXTRA_TARGET_MPP=0.25
bash examples/run_tcga_luad_egfr_end_to_end.sh20x only (no external)
export PROJECT_ID=TCGA-LUAD
export GENE=EGFR
export ENCODER=h-optimus-0
export RUN_NAME=TCGA-LUAD
export RUN_MODE=force
export PER_CLASS=0
export TARGET_MPP=0.5
export EXTRA_TARGET_MPP=
bash examples/run_tcga_luad_egfr_end_to_end.sh40x only (no external)
export PROJECT_ID=TCGA-LUAD
export GENE=EGFR
export ENCODER=h-optimus-0
export RUN_NAME=TCGA-LUAD
export RUN_MODE=force
export PER_CLASS=0
export TARGET_MPP=0.25
export EXTRA_TARGET_MPP=
bash examples/run_tcga_luad_egfr_end_to_end.shExternal inference (20x+40x)
export PROJECT_ID=TCGA-LUAD
export GENE=EGFR
export ENCODER=h-optimus-0
export RUN_NAME=TCGA-LUAD
export RUN_MODE=force
export PER_CLASS=0
export EXTERNAL_PER_CLASS=0
export TARGET_MPP=0.5
export EXTRA_TARGET_MPP=0.25
export EXTERNAL_MANIFEST=/path/to/external_manifest.csv
export EXTERNAL_ROOT=/path/to/foundation_model_training_images/EXTERNAL
bash examples/run_tcga_luad_egfr_end_to_end.shOptions (env vars)
PROJECT_ID(default:TCGA-LUAD)GENE(default:KRAS)ENCODER(default:h-optimus-0)DEVICE(default:cuda)TARGET_MPP(default:0.5)EXTRA_TARGET_MPP(default:0.25; comma-separated MPP buckets — each slide is assigned to the nearest bucket)RUN_NAME(default:${PROJECT_ID})RUN_MODEin{force|resume|rebuild}(default:force)force: reset derived outputs only (tiling/features/training/inference/etc.) and preservegdc_downloads/; re-downloads happen only if checksum/size validation fails.resume: keep all existing outputs; only run missing stages (downloads/tiling/features/training/inference).rebuild: keep downloads + derived labels, but re-run tiling, features, training, inference, external_inference, plots.
PER_CLASS,EXTERNAL_PER_CLASS,LIMIT_TILES,EPOCHS,PATIENCE,VAL_PER_CLASS(default:0= use full test split for validation)
By default, RUN_NAME is set to the project id so all outputs live under runs/<project-id>/.
All outputs land under runs/<project-id>/ (the default RUN_NAME is the project id).
A) Project inputs (download + derived labels)
runs/<project-id>/gdc_downloads/<PROJECT>_svs_manifest.tsv— full GDC SVS manifest for the projectruns/<project-id>/gdc_downloads/svs/**/**.svs— downloaded TCGA SVS files (GDC UUID folders)runs/<project-id>/gdc_downloads/maf/**/**.maf.gz— downloaded GDC mutation calls used for labelsruns/<project-id>/checkpoints/<TARGET>/manifests/<PROJECT>_<TARGET>_svs_manifest_subset.tsv— selected SVS rowsruns/<project-id>/checkpoints/<TARGET>/manifests/<PROJECT>_<TARGET>_slides.csv— selected slide list + labels (schema below)runs/<project-id>/checkpoints/<TARGET>/manifests/external_manifest_<TARGET>_<encoder>.csv— external subset manifest (schema below)
B) Tiling
runs/<project-id>/tiling/tiles_20x/manifests/<slide_id>_tiles.csv— per-slide tile coordinate manifest (schema below)runs/<project-id>/tiling/tiles_20x/cases/<case_id>_tiles.csv— per-case tile coordinates aggregated across slidesruns/<project-id>/tiling/tiles_20x/cases/cases_index.csv— case_id → case manifest path indexruns/<project-id>/tiling/tiles— alias pointing attiles_20xfor compatibilityruns/<project-id>/tiling/tile_coords_20x.csv— aggregated tile coords (x,y,slide,sample_id,target)runs/<project-id>/tiling/tiling_manifest.csv— slide inventory (file_name,sample_id,target,slide_path)
When EXTRA_TARGET_MPP is set (e.g., 0.25), slides are bucketed by native MPP:
runs/<project-id>/tiling/tiles_20x/...for slides closer to0.5runs/<project-id>/tiling/tiles_40x/...for slides closer to0.25runs/<project-id>/tiling/tile_coords_20x.csvandtile_coords_40x.csveach contain only the slides assigned to that bucket
Tiles keep the same physical size across buckets by scaling pixel size (e.g., 224px at 0.5 mpp ⇔ 448px at 0.25 mpp).
C) Features + QC
runs/<project-id>/features/<encoder>/features_<slide_id>.pt— per-slide feature tensor (N tiles × D; tile size set by the slide’s MPP bucket)runs/<project-id>/features/<encoder>/features_<slide_id>.json— QC metadata + checksums (schema below)- If feature extraction fails QC, tensors are renamed:
features_<slide_id>.FAILED_tile_count_mismatch.ptfeatures_<slide_id>.FAILED_degenerate_embeddings.pt
Feature tensors are saved as PyTorch .pt files with rows aligned to the slide’s tile manifest order,
so each row can be joined back to tile coordinates using the corresponding tile manifest CSV.
Performance note: the feature extractor auto-tunes GPU batch size based on available VRAM and uses
CPU data loader workers to keep the GPU fed. You can override both via --batch-size and --num-workers.
As a rule of thumb, start with --num-workers 4 and increase to 8–16 on beefy GPU nodes if CPU
tile decoding becomes the bottleneck.
When EXTRA_TARGET_MPP is set, features are still written once per slide (no suffix).
Slides assigned to 40x are extracted with the larger pixel tile size, but the output path remains:
runs/<project-id>/features/<encoder>/features_<slide_id>.pt
D) Training (5-fold CV)
AGGREGATORis the upper-case aggregation method (default:GMA).runs/<project-id>/training/checkpoints/<GENE>/versioned_split_manifest/<GENE>_all_splits_latest.csv— shared split manifest used for all encoders (schema below)runs/<project-id>/training/checkpoints/<GENE>/<ENCODER>/<AGGREGATOR>/classification_report/cv_summary.csv— per-split best epoch + metrics (schema below)runs/<project-id>/training/checkpoints/<GENE>/<ENCODER>/<AGGREGATOR>/split_1_set/checkpoint/checkpoint_best.pt— best checkpoint for that split (and similarly for split_2_set..split_5_set)
E) Per-split held-out test inference (attention export + ROC/PR plots)
Written under each split so split context is self-contained:
runs/<project-id>/training/checkpoints/<GENE>/<ENCODER>/<AGGREGATOR>/split_1_set/inference/test/inference_results.csvruns/<project-id>/training/checkpoints/<GENE>/<ENCODER>/<AGGREGATOR>/split_1_set/inference/test/attention/<slide_id>_attention.csvruns/<project-id>/training/checkpoints/<GENE>/<ENCODER>/<AGGREGATOR>/split_1_set/inference/test/plots/roc_pr_curves.png
Note: the training stage also writes probability exports under the same directory (e.g. probabilities_test_set.csv).
F) External inference (external LUAD)
External inference runs for each split using the best AUC epoch from CV plus any configured epochs (e.g., the final epoch), and evaluates every slide in the external cohort. Results are written under each split:
runs/<project-id>/training/checkpoints/<GENE>/<ENCODER>/<AGGREGATOR>/split_1_set/external_inference/external/ckpt_best_<epoch>/inference_results.csvruns/<project-id>/training/checkpoints/<GENE>/<ENCODER>/<AGGREGATOR>/split_1_set/external_inference/external/ckpt_best_<epoch>/attention/<slide_id>_attention.csvruns/<project-id>/training/checkpoints/<GENE>/<ENCODER>/<AGGREGATOR>/split_1_set/external_inference/external/ckpt_best_<epoch>/plots/roc_pr_curves.png
Additional epochs (e.g., final epoch) appear under:
.../external_inference/external/ckpt_<epoch>/...
TCGA slide selection manifest (training/checkpoints/<GENE>/manifests/<PROJECT>_<GENE>_slides.csv)
slide_id(e.g.TCGA-05-4244-01Z-00-DX1)slide_path(downloaded.svspath)label_index(0/1mutation label for the chosen gene)patient_id(TCGA case/patient barcode prefix)
Versioned split manifest (training/checkpoints/<GENE>/versioned_split_manifest/<GENE>_all_splits_latest.csv)
slide_path,slide_id,label_indextarget(legacy column retained for compatibility)split_1_set…split_5_setwith values in{train,val,test}
Tile manifest (tiling/tiles_20x/manifests/<slide_id>_tiles.csv)
slide_idtile_id(stable tile identifier)x,y(level-0 pixel coordinates)level(OpenSlide level used for extraction / overlays)width,height(tile size in pixels atlevel)tissue_fraction(fraction of tile kept after tissue mask filtering)tile_path(optional; blank when only coordinates are generated)
Tile manifest with attention (.../attention/<slide_id>_tiles_with_attention.csv)
- Same columns as the tile manifest, plus
attentionfor that slide.
Feature metadata JSON (features/<encoder>/features_<slide_id>.json)
slide_id,encodertile_manifestnum_tiles,num_features,feature_dimembedding_stats(degenerate/variance checks)feature_sha256,feature_bytesstatusin{ok,failed}andfailure_reasonwhen failed Metadata is written once per slide (no suffix), regardless of its MPP bucket.
CV summary (training/checkpoints/<GENE>/<ENCODER>/<AGGREGATOR>/classification_report/cv_summary.csv)
split(e.g.split_1_set)best_epochval_*andtest_*metrics, including*_roc_auc,*_accuracy,*_precision,*_recall,*_f1,*_balanced_error_rate
Inference results (.../inference_results.csv)
slide_idprobability(predicted P(class=1))prediction(thresholded at 0.5 by default)target(ground-truth label, fromlabel_index)
Attention exports (.../attention/<slide_id>_attention.csv)
slide_idtile_index(0-based)tile_id,x,y,level(when tile manifest metadata is available)attention(raw attention weight)probability(slide-level probability repeated for convenience)
External inference manifest (training/checkpoints/<GENE>/manifests/external_manifest_<GENE>_<encoder>.csv)
Minimum required columns for external inference with InferenceRunner:
slide_id(string id used to resolve features / label rows)label_index(0/1 ground-truth label; used astarget_column)
Strongly recommended columns (enables overlays + clearer provenance):
slide_path(absolute.svspath for overlays; optional if you do not generate overlays)feature_path(absolute.ptpath; optional if you pass--feature-dirand followfeatures_<slide_id>.ptnaming)
Optional columns:
split(only needed if you want to run inference on a subset viaInferenceConfig.split_column/split_value)
Docs:
docs/targets.mddocs/pipeline.md- SLURM (generic):
examples/slurm/submit_tcga_cv_to_external.sh - SLURM (EGFR example):
examples/slurm/submit_tcga_luad_EGFR_cv_to_external.sh
Each encoder below reflects the implementation in goldmark/features/canonical_sources.py, which follows the
recommended preprocessing from the original model repositories. Access typically requires a Hugging Face token.
prov-gigapath
Access
- Hugging Face access (may require approval); set
HF_TOKEN/HUGGINGFACE_HUB_TOKENor usehuggingface-cli login.
Hugging Face (more details)
Repo: prov-gigapath/prov-gigapath
URL: https://huggingface.co/prov-gigapath/prov-gigapath
Implementation
- Loader:
timm.create_model("hf_hub:prov-gigapath/prov-gigapath", pretrained=True) - Transform: resize 256 → center crop 224 → ImageNet normalize (timm defaults)
- Tile size: 224
- Feature dim: 1536
EAGLE
Access
- Public on Hugging Face; set
HF_TOKEN/HUGGINGFACE_HUB_TOKENif needed.
Hugging Face (more details)
Repo: MCCPBR/EAGLE
URL: https://huggingface.co/MCCPBR/EAGLE
Implementation
- Loader: use the custom encoder hook (
--custom-encoder/--custom-encoder-script) to point at the EAGLE implementation. - Transform: follow the EAGLE repo’s preprocessing exactly (see internal docs).
- Feature dim: defined by the EAGLE encoder (see internal docs).
EAGLE repo reference (example)
git clone --no-checkout https://huggingface.co/MCCPBR/EAGLE && cd EAGLEfrom PIL import Image
import numpy as np
import eagle
import torch
import torchvision.transforms as transforms
# Load model
model = eagle.EAGLE()
# Set up transform
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.485, 0.456, 0.406), (0.229, 0.224, 0.225))
])
# Image
img = np.random.randint(0, 256, size=224*224*3).reshape(224,224,3).astype(np.uint8)
img = Image.fromarray(img)
img = transform(img).unsqueeze(0)
# Inference
with torch.no_grad():
h, att, p = model(img)UNI
Access
- Hugging Face access (gated); set
HF_TOKEN/HUGGINGFACE_HUB_TOKENor usehuggingface-cli login. - If you already have weights locally, point
MIL_UNI_CHECKPOINTto the file (fallback:weights/uni_checkpoint.pth).
Hugging Face (more details)
Repo: MahmoodLab/UNI
URL: https://huggingface.co/MahmoodLab/UNI
Implementation
- Loader:
timm.create_model("vit_large_patch16_224", ...)+ load checkpoint - Transform: resize 224 → ImageNet normalize (mean=(0.485,0.456,0.406), std=(0.229,0.224,0.225))
- Tile size: 224
- Feature dim: 1024
Virchow
Access
- Hugging Face access (may require approval); set
HF_TOKEN/HUGGINGFACE_HUB_TOKENor usehuggingface-cli login.
Hugging Face (more details)
Repo: paige-ai/Virchow
URL: https://huggingface.co/paige-ai/Virchow
Implementation
- Loader:
timm.create_model("hf-hub:paige-ai/Virchow", ...)withSwiGLUPacked+SiLU - Transform:
timm.create_transformfrom model config - Tile size: 224 (0.5 mpp recommended by model card)
- Feature dim: 2560 (class token + pooled patch tokens)
Virchow2
Access
- Hugging Face access (may require approval); set
HF_TOKEN/HUGGINGFACE_HUB_TOKENor usehuggingface-cli login.
Hugging Face (more details)
Repo: paige-ai/Virchow2
URL: https://huggingface.co/paige-ai/Virchow2
Implementation
- Loader:
timm.create_model("hf-hub:paige-ai/Virchow2", ...)withSwiGLUPacked+SiLU - Transform:
timm.create_transformfrom model config - Tile size: 224 (0.5 mpp recommended by model card)
- Feature dim: 2560 (class token + pooled patch tokens; ignores 4 register tokens)
h-optimus-0
Access
- Hugging Face access (gated; requires explicit approval).
Hugging Face (more details)
Repo: bioptimus/H-optimus-0
URL: https://huggingface.co/bioptimus/H-optimus-0
Implementation
- Loader:
timm.create_model("hf-hub:bioptimus/H-optimus-0", pretrained=True) - Transform: resize 224 → normalize Bioptimus mean/std (mean=(0.707223,0.578729,0.703617), std=(0.211883,0.230117,0.177517))
- Tile size: 224 (0.5 mpp recommended by model card)
- Feature dim: 1536
| Path | What it contains |
|---|---|
targets/ |
Public, token-safe scripts for GDC download + mutation target tables |
goldmark/ |
Minimal Python package: tiling, feature extraction, training, inference |
scripts/ |
Manuscript-scale runners (CV scanning, inference plans, attention export) |
examples/ |
Example manifest headers + plan templates |
paper/ |
Select manuscript figures/tables for reference |
Preferred (conda):
source /home/vanderbc/.bashrc
bash scripts/setup_conda_goldmark_env.shIf you have sudo and prefer a venv:
bash scripts/setup_goldmark_sudo.shSet a repo root (recommended):
export REPO_ROOT="$PWD"Generate a GDC manifest (SVS example):
python -m goldmark gdc-manifest svs \
--project-id TCGA-COAD \
--out tcga_coad_svs_manifest.tsvDownload data from GDC:
targets/tcga/gdc_download.sh --manifest tcga_coad_svs_manifest.tsv --out data/gdc_download
# If needed for controlled-access data, set GDC_TOKEN_FILE in configs/secrets.env
# (or pass --token /path/to/gdc_token.txt).Annotate MAF with OncoKB (token via env var):
# export ONCOKB_TOKEN="..." # or set in configs/secrets.env
python targets/variants/annotate_maf_oncokb_by_hgvsg.py \
--maf-glob "data/gdc_download/**/**.maf.gz" \
--oncotree-code <ONCOTREE_CODE> \
--output data/oncokb/oncokb_annotations.csv--oncotree-code ensures OncoKB levels are computed for the correct tumor type
(e.g., LUAD for TCGA-LUAD).
Summarize a single gene’s patient-level mutation status:
python targets/variants/summarize_gene_status.py \
--annotations data/oncokb/oncokb_annotations.csv \
--gene <GENE> \
--output data/targets/<GENE>_patient_labels.csvThe output includes per-patient p. changes and OncoKB level summaries for spot-checking:
p_changes(joined with|for multiple mutations;no_gene_changesif none)oncokb_levels(joined with|)oncokb_positive/has_gene_mutation/label_reason
label_index is positive if any mutation is OncoKB level 1/2/3 for the target gene.
Build a slide-level manifest by joining SVS paths to patient labels:
python targets/tcga/build_slide_manifest_from_svs_and_mutations.py \
--svs-root data/gdc_download \
--labels data/targets/<GENE>_patient_labels.csv \
--output data/manifests/TCGA_<GENE>_slide_manifest.csvGenerate the manuscript-style split manifest:
PYTHONPATH=. python scripts/generate_versioned_split_manifest.py \
--manifest data/manifests/TCGA_<GENE>_slide_manifest.csv \
--target <GENE> \
--label-column label_index \
--target-dir data/checkpoints/<GENE>See manifest header examples:
- TCGA split manifest schema:
examples/manifests/tcga_split_manifest_header.csv - External manifest schema (header only):
examples/manifests/external_LUAD_manifest_header.csv
The canonical tiler is goldmark/tiling/extractor.py. For convenience, this repo also exposes a
small CLI:
PYTHONPATH=. python -m goldmark tiling data/manifests/TCGA_<GENE>_slide_manifest.csv \
--output runs \
--run-name tcga_<GENE>_demo \
--tile-size 224 \
--stride 224 \
--target-mpp 0.5Tile manifests are written under runs/<run-name>/tiling/tiles/manifests/ as <slide_id>_tiles.csv.
Feature extraction writes:
features_<slide_id>.pt(per-slide tensor)features_<slide_id>.json(QC metadata + checksums)
If tile/feature counts mismatch or embeddings are degenerate (low variance), the tensor is renamed:
features_<slide_id>.FAILED_<reason>.pt and training/inference will treat the slide as missing.
Example:
PYTHONPATH=. python -m goldmark features data/manifests/TCGA_<GENE>_slide_manifest.csv \
--tile-manifests runs/tcga_<GENE>_demo/tiling/tiles \
--output runs \
--run-name tcga_<GENE>_demo \
--encoder prov-gigapath \
--device cuda \
--num-workers 4Optional integrity audit (tile counts vs feature lengths):
python scripts/check_tile_feature_counts.py \
--tile-manifest-dir runs/tcga_<GENE>_demo/tiling/tiles/manifests \
--feature-dir runs/tcga_<GENE>_demo/features/prov-gigapathManuscript-scale training is orchestrated by:
scripts/run_training_scan_target.py(scan encoders + submit missing runs)scripts/train_task_v2.py(train one target across 5 splits)
The manuscript pipeline exports attention vectors for (i) best checkpoint and (ii) a fixed late epoch to show how checkpoint selection affects results and to support consistent downstream overlays.
See:
scripts/run_inference_from_plan.pyscripts/gma_inference_pipeline.py- SLURM template:
examples/slurm/submit_attn_external_BLCA_ERBB2.sh
- Workflow:
paper/figures/workflow_schematic.pdf - Tile/feature QC:
paper/figures/qc_tile_feature_integrity.pdf - Table 1 (counts):
paper/tables/table1_gene_mutation_counts.tex
Show tumor/target tasks and cohort counts
| Tumor Code | Tumor Type | Gene Mutation | MSKCC Total | MSKCC Positive | MSKCC Negative | TCGA Total | TCGA Positive | TCGA Negative |
|---|---|---|---|---|---|---|---|---|
| BLCA | Bladder Urothelial Carcinoma | PIK3CA | 2031 | 337 | 1694 | 386 | 77 | 309 |
| BLCA | Bladder Urothelial Carcinoma | FGFR3 | 2031 | 393 | 1638 | 386 | 34 | 352 |
| BLCA | Bladder Urothelial Carcinoma | ERBB2 | 2031 | 207 | 1824 | 386 | 37 | 349 |
| BLCA | Bladder Urothelial Carcinoma | TSC1 | 2031 | 131 | 1900 | 386 | 27 | 359 |
| BLCA | Bladder Urothelial Carcinoma | ERCC2 | 2031 | 141 | 1890 | 386 | 28 | 358 |
| BRCA | Breast Carcinoma | PIK3CA | 2735 | 966 | 1769 | 1000 | 354 | 646 |
| CESC | Cervical Squamous Cell Carcinoma | PIK3CA | 171 | 62 | 109 | 266 | 82 | 184 |
| COAD | Colon Adenocarcinoma | PIK3CA | 2959 | 605 | 2354 | 550 | 130 | 420 |
| COAD | Colon Adenocarcinoma | BRAF | 2959 | 318 | 2641 | 550 | 59 | 491 |
| COAD | Colon Adenocarcinoma | ATM | 2959 | 153 | 2806 | 550 | 63 | 487 |
| COAD | Colon Adenocarcinoma | PTEN | 2959 | 183 | 2776 | 550 | 38 | 512 |
| COAD | Colon Adenocarcinoma | MSI | 2959 | 350 | 2609 | 405 | 70 | 335 |
| GBM | Glioblastoma Multiforme | PTEN | 816 | 263 | 553 | 244 | 77 | 167 |
| UCEC | Endometrial Cancer | PTEN | 2062 | 972 | 1090 | 499 | 319 | 180 |
| UCEC | Endometrial Cancer | PIK3CA | 2062 | 904 | 1158 | 499 | 254 | 245 |
| UCEC | Endometrial Cancer | FBXW7 | 2062 | 279 | 1783 | 499 | 94 | 405 |
| UCEC | Endometrial Cancer | ATM | 2062 | 142 | 1920 | 499 | 73 | 426 |
| UCEC | Endometrial Cancer | POLE | 2062 | 114 | 1948 | 499 | 49 | 450 |
| LGG | Glioma | IDH1 | 449 | 216 | 233 | 491 | 382 | 109 |
| LGG | Glioma | PIK3CA | 449 | 41 | 408 | 491 | 42 | 449 |
| HNSC | Head and Neck Carcinoma | PIK3CA | 485 | 84 | 401 | 431 | 66 | 365 |
| HNSC | Head and Neck Carcinoma | HRAS | 485 | 11 | 474 | 431 | 25 | 406 |
| LUAD | Lung Adenocarcinoma | KRAS | 923 | 273 | 650 | 465 | 171 | 294 |
| LUAD | Lung Adenocarcinoma | EGFR | 923 | 273 | 650 | 465 | 51 | 414 |
| SKCM | Melanoma | BRAF | 886 | 213 | 673 | 432 | 233 | 199 |
| SKCM | Melanoma | NRAS | 886 | 143 | 743 | 432 | 112 | 320 |
| SKCM | Melanoma | PTEN | 886 | 63 | 823 | 432 | 50 | 382 |
| SKCM | Melanoma | MAP2K1 | 886 | 38 | 848 | 432 | 26 | 406 |
| PAAD | Pancreatic Adenocarcinoma | KRAS | 3018 | 2674 | 344 | 181 | 136 | 45 |
| PCPG | Pheochromocytoma/Paraganglioma | HRAS | 18 | 2 | 16 | 176 | 19 | 157 |
| STAD | Stomach Adenocarcinoma | PIK3CA | 742 | 69 | 673 | 374 | 60 | 314 |
| THCA | Thyroid Cancer | BRAF | 920 | 399 | 521 | 495 | 296 | 199 |
| THCA | Thyroid Cancer | NRAS | 920 | 130 | 790 | 495 | 41 | 454 |
This repo includes a convenience launcher that loops over the manuscript task list and calls
scripts/run_training_scan_target.py for each tumor/target.
export MIL_DATA_ROOT="/path/to/foundation_model_training_images"
# dry-run (prints commands)
python scripts/launch_manuscript_tasks.py
# execute (runs training scans)
python scripts/launch_manuscript_tasks.py --executeNote: this wrapper runs commands sequentially. On HPC clusters you will typically wrap each printed command
in sbatch (or your scheduler of choice).
1) Run a full project with slides from GDC (default)
This path starts from GDC slide download and runs:
download -> tiling -> features -> training -> inference
For concrete command examples, use the detailed sections above:
### Recommended: TCGA-LUAD KRAS (5×70/30 splits) → external LUAD inferenceSLURM (GPU example)accordion (examples/slurm/submit_tcga_cv_to_external.sh)nohup (interactive node)accordion (scripts/nohup_tcga_cv_to_external.sh)End-to-end example (foreground)accordion (examples/run_tcga_luad_egfr_end_to_end.sh)
Primary orchestration script:
scripts/tcga_cv_to_external_full_run.py
Run outputs:
runs/<RUN_NAME>/(full artifacts)runs/<RUN_NAME>/training/checkpoints/(trained checkpoints)runs/<RUN_NAME>/inference/(inference outputs)
Public GOLDMARK reference links:
- Landing page: https://artificialintelligencepathology.org/
- Runs API: https://artificialintelligencepathology.org/api/runs
- Example TCGA run: https://artificialintelligencepathology.org/runs/rf207d22e2c0c~TCGA-BLCA_svs
2) Run a full project from pre-extracted features with GOLDMARK API
This path reuses existing GOLDMARK training code (scripts/train_task_v2.py) and only adds API orchestration.
The helper script:
- discovers downloadable artifacts from
https://artificialintelligencepathology.org/api/runs/{slug}/downloads - downloads the sanitized gene split manifest bundle
- resolves feature tensors (local extracted dir when available, or downloaded feature zip)
- launches
scripts/train_task_v2.pywithAGGREGATION_METHOD=gma
Script:
scripts/api_demo_from_preextracted_features.py
BLCA + FGFR3 + UNI demo:
python scripts/api_demo_from_preextracted_features.py \
--base-url https://artificialintelligencepathology.org \
--slug rf207d22e2c0c~TCGA-BLCA_svs \
--gene FGFR3 \
--encoder uni \
--work-dir runs/api_demo_blca_fgfr3_uni \
--sample-per-class 40 \
--epochs 5 \
--device cudaUseful API links for this workflow:
- Runs list: https://artificialintelligencepathology.org/api/runs?dataset=tcga
- Download catalog (BLCA example): https://artificialintelligencepathology.org/api/runs/rf207d22e2c0c~TCGA-BLCA_svs/downloads?download_source=postgres&results_mode=publication&epoch_view=best
- Category-specific API instructions (manifest): https://artificialintelligencepathology.org/download-api-instructions?slug=rf207d22e2c0c~TCGA-BLCA_svs&category=manifest&results_mode=publication&epoch_view=best&download_source=postgres
- Category-specific API instructions (features): https://artificialintelligencepathology.org/download-api-instructions?slug=rf207d22e2c0c~TCGA-BLCA_svs&category=features&results_mode=publication&epoch_view=best&download_source=postgres
Download scheme for direct artifacts:
- Fetch catalogs from
https://artificialintelligencepathology.org. Catalog itemdownload_urlvalues may be absolute URLs onhttps://downloads.artificialintelligencepathology.org, so do not blindly prependBASE_URL. - Derive the local filename from
pathorrelative_pathin the catalog and pass it explicitly with-o. Do not combinecurl --continue-at -with-J/--remote-header-name; curl rejects that combination. - For large feature archives, use resumable curl with retries and a local log. If a transfer drops, rerun the same command from the same directory;
--continue-at -resumes the partial file.
Example: list feature bundle names and URLs for BLCA:
BASE_URL="https://artificialintelligencepathology.org"
RUN_SLUG="rf207d22e2c0c~TCGA-BLCA_svs"
CATALOG_URL="${BASE_URL}/api/runs/${RUN_SLUG}/downloads?download_source=postgres&results_mode=publication&epoch_view=best"
curl -fsS "${CATALOG_URL}" | jq -r --arg base "${BASE_URL}" '
.categories[]
| select(.key=="features")
| .items[]
| select(.download_url != null)
| [
.label,
((.path // .relative_path // (.label + ".download")) | split("/") | .[-1]),
(if (.download_url | startswith("http")) then .download_url else ($base + .download_url) end)
]
| @tsv
'Example: resumable download of the first item in a category:
BASE_URL="https://artificialintelligencepathology.org"
RUN_SLUG="rf207d22e2c0c~TCGA-BLCA_svs"
CATEGORY="manifest"
CATALOG_URL="${BASE_URL}/api/runs/${RUN_SLUG}/downloads?download_source=postgres&results_mode=publication&epoch_view=best"
LOG="goldmark_download_$(date -u +%Y%m%dT%H%M%SZ).log"
FIRST_ROW=$(curl -fsS "${CATALOG_URL}" | jq -r --arg base "${BASE_URL}" --arg category "${CATEGORY}" '
.categories[]
| select(.key==$category)
| .items[]
| select(.download_url != null)
| [
((.path // .relative_path // (.label + ".download")) | split("/") | .[-1]),
(if (.download_url | startswith("http")) then .download_url else ($base + .download_url) end)
]
| @tsv
' | head -n 1)
FILENAME=$(printf '%s\n' "${FIRST_ROW}" | cut -f1)
DOWNLOAD_URL=$(printf '%s\n' "${FIRST_ROW}" | cut -f2-)
{
printf 'started_utc=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
printf 'filename=%s\nurl=%s\n' "${FILENAME}" "${DOWNLOAD_URL}"
curl --http1.1 -fL --continue-at - -o "${FILENAME}" \
--retry 1000 --retry-all-errors --retry-delay 30 --connect-timeout 30 \
--speed-time 300 --speed-limit 1024 \
--write-out '\nhttp_code=%{http_code}\nremote_ip=%{remote_ip}\nsize_download=%{size_download}\ntime_total=%{time_total}\nurl_effective=%{url_effective}\n' \
"${DOWNLOAD_URL}"
printf 'finished_utc=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
} 2>&1 | tee -a "${LOG}"Notes:
- On the same host where
/data3/vanderbc/foundation_model_training_images/...is mounted, keep--prefer-local-feature-dir(default) to avoid re-downloading very large feature zips. - On a remote host, use
--no-prefer-local-feature-dirand optionally--extract-all-featureswhen you need the full feature archive.
3) Run Inference on User Slides (requires mutations curation)
This workflow is for external engineers running GOLDMARK on their own .svs slides.
Important constraints:
- You must curate mutation-derived labels outside this repo and provide binary labels (
Positive/Negative->1/0) per task. - We do not provide one canonical OncoKB labeling script for external data because source formats differ across institutions.
- Given a labeled manifest + slides, GOLDMARK can run
tiling -> features -> inferenceand consume slide-level checkpoints from the public API.
Minimum recommended columns:
slide_id(stable per-slide id)slide_path(absolute path to.svs)label_index(0or1)split(set all rows totestfor pure inference)
Example:
slide_id,slide_path,label_index,split
CASE_001,/absolute/path/CASE_001.svs,1,test
CASE_002,/absolute/path/CASE_002.svs,0,testPYTHONPATH=. python -m goldmark tiling user_manifest.csv \
--output runs \
--run-name user_slides_fgfr3 \
--tile-size 224 \
--stride 224 \
--target-mpp 0.5
PYTHONPATH=. python -m goldmark features user_manifest.csv \
--tile-manifests runs/user_slides_fgfr3/tiling/tiles \
--output runs \
--run-name user_slides_fgfr3 \
--encoder uni \
--device cuda \
--num-workers 4You may switch --encoder to any canonical encoder (gigapath_ft, h-optimus-0, prov-gigapath, uni, virchow, virchow2).
The checkpoint catalog is exposed from:
The command below selects the checkpoint with the highest external_auc for one (gene, encoder) pair
(fallback to cv_auc if external_auc is missing):
BASE_URL="https://artificialintelligencepathology.org"
RUN_SLUG="rf207d22e2c0c~TCGA-BLCA_svs"
GENE="FGFR3"
ENCODER="uni"
export GENE ENCODER
DOWNLOADS_JSON="$(mktemp)"
export DOWNLOADS_JSON
curl -sS "${BASE_URL}/api/runs/${RUN_SLUG}/downloads?download_source=postgres&results_mode=publication&epoch_view=best" > "${DOWNLOADS_JSON}"
BEST_JSON="$(python - <<'PY'
import json,sys,re,os
obj=json.load(open(os.environ["DOWNLOADS_JSON"]))
gene=os.environ["GENE"].upper()
enc=os.environ["ENCODER"].lower()
def norm(x): return re.sub(r'[^a-z0-9]+','',str(x).lower())
rows=[]
for cat in obj.get("categories",[]):
if cat.get("key")!="slide_models":
continue
for it in cat.get("items",[]):
m=it.get("metadata") or {}
if str(m.get("gene","")).upper()!=gene:
continue
if norm(m.get("encoder",""))!=norm(enc):
continue
score=m.get("external_auc")
if score is None:
score=m.get("cv_auc")
rows.append((float(score) if score is not None else -1.0, it))
if not rows:
raise SystemExit("No matching slide_models found.")
rows.sort(key=lambda t:t[0], reverse=True)
print(json.dumps(rows[0][1]))
PY)"
export BEST_JSON
CKPT_URL="$(python -c 'import json,os,urllib.parse; u=json.loads(os.environ["BEST_JSON"])["download_url"]; b=os.environ["BASE_URL"].rstrip("/") + "/"; print(u if u.startswith(("http://","https://")) else urllib.parse.urljoin(b, u.lstrip("/")))')"
mkdir -p models
curl --http1.1 -fL --continue-at - -o "models/${GENE}_${ENCODER}_best_external_auc.pt" "${CKPT_URL}"
rm -f "${DOWNLOADS_JSON}"GENE="FGFR3"
ENCODER="uni"
PYTHONPATH=. python -m goldmark inference user_manifest.csv \
--feature-dir "runs/user_slides_fgfr3/features/${ENCODER}" \
--checkpoint "models/${GENE}_${ENCODER}_best_external_auc.pt" \
--output runs \
--run-name "user_slides_fgfr3_${ENCODER}_inference" \
--target label_index \
--split-column split \
--split-value test \
--export-attention \
--no-overlaysIf you want split-by-split checkpoint sweeps (instead of default “best external AUC” only):
BASE_URL="https://artificialintelligencepathology.org"
RUN_SLUG="rf207d22e2c0c~TCGA-BLCA_svs"
GENE="FGFR3"
ENCODER="uni"
export GENE ENCODER
DOWNLOADS_JSON="$(mktemp)"
export DOWNLOADS_JSON
curl -sS "${BASE_URL}/api/runs/${RUN_SLUG}/downloads?download_source=postgres&results_mode=publication&epoch_view=best" > "${DOWNLOADS_JSON}"
python - <<'PY' > /tmp/checkpoints.tsv
import json,sys,re,os
obj=json.load(open(os.environ["DOWNLOADS_JSON"]))
gene=os.environ["GENE"].upper()
enc=os.environ["ENCODER"].lower()
def norm(x): return re.sub(r'[^a-z0-9]+','',str(x).lower())
for cat in obj.get("categories",[]):
if cat.get("key")!="slide_models":
continue
for it in cat.get("items",[]):
m=it.get("metadata") or {}
if str(m.get("gene","")).upper()!=gene:
continue
if norm(m.get("encoder",""))!=norm(enc):
continue
split=str(m.get("split","na"))
ext=m.get("external_auc")
cv=m.get("cv_auc")
url=it['download_url']
base=os.environ.get("BASE_URL", "https://artificialintelligencepathology.org").rstrip("/") + "/"
if not str(url).startswith(("http://", "https://")):
import urllib.parse
url=urllib.parse.urljoin(base, str(url).lstrip("/"))
print(f"{split}\t{ext}\t{cv}\t{url}")
PY
rm -f "${DOWNLOADS_JSON}"
while IFS=$'\t' read -r SPLIT EXT_AUC CV_AUC URL; do
OUT_CKPT="models/${GENE}_${ENCODER}_split${SPLIT}.pt"
curl --http1.1 -fL --continue-at - -o "${OUT_CKPT}" "${URL}"
PYTHONPATH=. python -m goldmark inference user_manifest.csv \
--feature-dir runs/user_slides_fgfr3/features/uni \
--checkpoint "${OUT_CKPT}" \
--output runs \
--run-name "user_slides_fgfr3_uni_split${SPLIT}" \
--target label_index \
--split-column split \
--split-value test \
--no-overlays
done < /tmp/checkpoints.tsv- This repo intentionally does not ship raw WSIs or protected clinical data.
- OncoKB and GDC access are governed by their respective terms; keep tokens out of git history.
- The codebase is built around downloadable artifacts and reproducible workflows exposed via the GOLDMARK API: https://artificialintelligencepathology.org/
