PANDA (Pan-tissue Adversarial Normalized Domain-invariant Anchored MLP) is a
compact prototype-anchored MLP classifier for scRNA-seq cell identity across skin,
hematopoietic, and pancreatic tissues, trained under a composite loss (supervised-
contrastive + VICReg + sub-center angular prototype-InfoNCE + gradient-reversal
dataset/depth adversary + HSIC depth-decorrelation). Two
input variants ship out of the box: PANDA-PCA (PCA(50) -> trunk) and
PANDA-Marker ([PCA(50) || marker_expr] -> trunk); the marker channel's gain
is system-dependent, largest on pancreas (+4.2% CV accuracy).
This README is a complete recipe to reproduce every result in PAPER.tex from a
clean clone. All commands are copy-pasteable and run from the repo root; scripts
resolve the repo root automatically (see PANDA_ROOT below).
Repository layout:
panda/ model + losses + panda/markers.yaml
scripts/pan_skin/ skin pipeline: download -> corpus -> train -> CV -> zero-shot
scripts/pancreas/ pancreas pipeline (same shape)
scripts/hematopoiesis/ HSC pipeline (same shape)
scripts/common/ system-agnostic train / CV / zero-shot drivers
scripts/analysis/ downstream discovery + interpretability
scripts/figures/ paper + supplement figure builders
data/corpus/{sys}/ downloaded + harmonized data
data/raw/ per-dataset raw counts (not in git)
checkpoints/{sys}/{variant}/panda_final.pt
discovery/{sys}/{variant}/*.json,*.csv all quantitative artefacts
figures/ fig1_..fig6, PANDA_supplement.pdf, biology/*.pdf
1. Requirements
Python: 3.9+ (project is tested on 3.10).
CUDA: PyTorch 2.6.0 wheels — CUDA 12.1/12.4 runtime works.
GPU: any single CUDA GPU works — the trainer is single-device (batch 256,
a compact MLP; a consumer GPU trains a checkpoint in well under an hour).
Optional extras: viz (plotly), dev (pytest, ruff, mypy).
Install:
bash
1git clone <this-repo> panda &&cd panda
2pip install -e .3# or, editable dev install:4pip install -e ".[dev]"
Repository root resolution (PANDA_ROOT)
Every script resolves the repository root as
os.environ.get("PANDA_ROOT", <two directories above the script>), so a clone at
any path works out of the box. Set PANDA_ROOT explicitly only if you run scripts
from a copied/vendored location or want data/checkpoints rooted elsewhere:
export PANDA_ROOT=/path/to/clone # optional
Data and checkpoints are expected at $PANDA_ROOT/data and $PANDA_ROOT/checkpoints
(symlink/junction these to the Hugging Face mirror download if you keep it separate).
1.1 LD_LIBRARY_PATH prefix (required for every PyTorch invocation)
PyTorch 2.6 sparse ops load libcusparseLt.so.0 which sits under the pip-installed
nvidia-cusparselt-cu12 package, and scanpy needs a modern libstdc++. Both
paths must be exported at the shell level, before Python starts:
If you also have a conda env that ships a newer libstdc++, prepend it:
bash
1# example — path is machine-specific; drop it if your system libstdc++ is >= 3.4.302exportLD_LIBRARY_PATH="/home/bcheng/.conda/pkgs/libstdcxx-15.2.0-h39759b7_7/lib:$LD_LIBRARY_PATH"
Every bash scripts/*/run_all.sh driver applies the same export automatically.
2. Data acquisition
Every URL below is a public GEO/ArrayExpress FTP link. Raw data is not
committed — it must be re-downloaded before anything else runs. Corpus builders
expect files at data/corpus/{system}/tier_{a,b,c,v2}/.
Pan-skin (6 studies, 45,387 cells)
Study
GEO
Role
Sulic 2023 (E14.5 dorsal)
GSE212673
anchor + held-out zero-shot
Dingwall 2024 (En1-cKO)
GSE220977
discovery target (paired with Aldrich GSE214695)
Belote 2021 (human melanocyte)
GSE151091
melanocyte anchor + held-out zero-shot
Haensel/Annusver 2020
GSE142471
adult homeostasis + wound
Joost 2016
GSE67602
Smart-seq2 platform anchor
Sennett 2015 (bulk RNA)
GSE70288
placode/dermal-condensate marker reference
Han MCA 2018 (neonatal skin)
GSE108097
Microwell-seq low-depth anchor
Merkel 2022
GSE201447
touch dome / volar biology
Aldrich 2023 (paired with Dingwall)
GSE214695
En1-cKO snRNA-seq
bash
1bash scripts/pan_skin/01_download_tier_a.sh # Aldrich, Ge/Gupta, Joost, Haensel2bash scripts/pan_skin/02_download_tier_b.sh # MCA, WIHN, Ge/Fuchs, Merkel3bash scripts/pan_skin/03_download_tier_c.sh # Sennett, Tie, Wiedemann (bulk + human)4# Dingwall / Sulic / Belote must be placed in data/raw/ manually — see repo notes
Pan-hematopoietic (3 studies used in the paper, 192,833 cells)
Wall-clock: 2-6 h depending on bandwidth (GSE108097 MCA tar is ~9 GB, GSE140802
Weinreb is ~14 GB, GSE114412 Veres is ~4 GB).
3. Corpus build (per system)
Each system builds a data/corpus/{system}/harmonized/corpus.h5ad plus a shared
HVG list, per-HVG mean/std, and a fitted PCA basis. Corpus is 100% paper-labeled;
every cell carries a label from its source paper's supplementary table.
writes data/corpus/hematopoiesis/held_out_labeled/nestorowa_GSE81682_test.h5ad
and data/corpus/pan_skin/held_out_labeled/sulic_GSE212673_test.h5ad.
4. Training
The canonical trainer is system-agnostic. It reads
data/corpus/{system}/harmonized/corpus.h5ad and writes
checkpoints/{system}/{variant}/panda_final.pt.
bash
1# 6 checkpoints total (3 systems x 2 variants). ~30-60 min each on 1x A100.2python -m scripts.common.train_panda pan_skin --variant pca --epochs 83python -m scripts.common.train_panda pan_skin --variant marker --epochs 84python -m scripts.common.train_panda hematopoiesis --variant pca --epochs 85python -m scripts.common.train_panda hematopoiesis --variant marker --epochs 86python -m scripts.common.train_panda pancreas --variant pca --epochs 87python -m scripts.common.train_panda pancreas --variant marker --epochs 8
Legacy per-system entry points also exist and are functionally equivalent for
skin/HSC/pancreas single-variant training:
scripts/pan_skin/20_train_panda.py, scripts/hematopoiesis/05_train_panda.py,
scripts/pancreas/05_train_panda.py. Prefer scripts.common.train_panda.
5. Held-out 5-fold cross-validation (Table 1)
The paper's Table 1 CV block reads
discovery/{system}/{variant}/cv_5fold.json. Two drivers exist:
scripts/common/run_cv.py — canonical, 5 epochs per fold, matches
paper numbers (mean acc / F1 / AUROC + per-class report).
scripts/common/cv_holdout.py — same architecture but supports GroupKFold
by dataset and a fuller 6-8 epoch curriculum; slower.
Both accept --systems and --variants:
bash
1# canonical 5-fold CV for all 3 systems x 2 variants2python -m scripts.common.run_cv --folds 5 --epochs 5
Per-system CV drivers also exist (scripts/pan_skin/40_heldout_5fold_cv.py,
scripts/hematopoiesis/07_heldout_5fold_cv.py,
scripts/pancreas/07_heldout_5fold_cv.py); they are single-variant, single-
system alternatives.
Multi-seed rigor
Seed replicates use the canonical driver on the canonical corpora; non-zero seeds
write cv_5fold_seed{N}.json next to the seed-0 cv_5fold.json:
Correction (2026-08): older cv_5fold_seed{1,2}.json files in the repo history
were produced on different corpus builds (different cell counts and label
vocabularies) by cv_holdout.py with a different curriculum; the "33/35
fold-comparisons" claim previously derived from them was invalid and has been
withdrawn. The seeds committed now are regenerated with the commands above on
the canonical corpora (verified same n/K per configuration). Honest result:
Marker beats PCA on 34/45 fold-comparisons — pancreas 15/15 (+0.04 acc
every seed), skin 11/15 (gap ≤0.003), hematopoiesis 8/15 (parity).
6. Held-out labeled zero-shot targets (Section 5)
One driver runs every zero-shot target for both variants:
1python scripts/analysis/70_prototype_geometry.py # intra + cross-system cosine + eff-dim2python scripts/analysis/72_emergent_axes.py # within-class PCA of 128-d z3python scripts/analysis/80_prototype_gene_attribution.py # integrated gradients per prototype4python scripts/analysis/81_counterfactual_knockouts.py # per-gene KO delta on cosine5python scripts/analysis/82_gene_coattribution_modules.py # gene co-attribution modules6python scripts/analysis/83_prototype_training_trajectory.py # prototype drift across curriculum7python scripts/analysis/84_adversary_purification.py # test GRL adversary is at chance8python scripts/analysis/85_hessian_gene_interactions.py # second-order gene pair Hessian9python scripts/analysis/63_nestorowa_zero_shot.py # Nestorowa unlabeled discovery
8. Figures + supplement
Main-text figures (figures/fig{1,2,3,4}_*.pdf)
bash
1python scripts/figures/generate_paper_figures.py
2# fig1_perclass_f1.pdf 3-panel per-class held-out F1 bars3# fig3_dahlin_heatmap.pdf within-class module-score heatmap4# fig4_veres_stage_stack.pdf Veres per-stage class fractions5# (there is no fig2 in the current paper; numbering is historical)
Figures 5/6 (En1-cKO + Kit-W41 recap) are built by the biology page pipeline
below — the standalone regen_fig5_fig6.py referenced in older notes is not in
the current tree; use the biology pipeline instead.
Supplement (figures/PANDA_supplement.pdf)
bash
1python scripts/figures/build_pca_vs_marker_umaps.py # PCA vs Marker UMAPs per target2python scripts/figures/build_figure_supplement.py # combined supplement PDF
Biology deep-dive supplement pages
Cache UMAPs once, then build per-topic pages, then merge into the supplement:
libcusparseLt.so.0: cannot open shared object file — you forgot to
export LD_LIBRARY_PATHbefore Python started. The pip-installed
nvidia-cusparselt-cu12 provides the library; PyTorch does not add its
path to the loader search. See section 1.1.
GLIBCXX_3.4.30 not found — your system libstdc++ is too old; prepend
a newer libstdc++.so.6's directory to LD_LIBRARY_PATH.
Dingwall GSM -> genotype mapping (frequent bug source): the correct map is
WT = {GSM6833478, GSM6833479, GSM6833480, GSM6833481},
cKO = {GSM6833482, GSM6833483}.
GSM6833480/481 are rttaControl (Cre-negative WT), not cKO. Getting this
wrong flips every En1-cKO enrichment sign.
Pancreas HVG builder OOM — scripts/pancreas/03_shared_hvgs_and_pca.py
peaks near 40 GB RAM on the 6-study union. Run on a node with >= 64 GB.
data/raw not in git — it is git-ignored (14+ GB of GEO tars). Rerun
section 2 to repopulate.
stratified split failure on rare classes — small-support classes
(< 2 members per fold) are merged into the parent canonical_label. If a
fold still errors, check that corpus.h5ad's canonical_label column has
the expected vocabulary; the paper vocab is the union enumerated in
PAPER.tex Sec 3.
n_conditions mismatch — PANDAEncoder reads it from
len(datasets) in the checkpoint; regenerate the checkpoint if you have
added/removed a dataset.
ContrastiveSampler in 1-condition data — auto-disabled when there is
only one condition; no config change needed.
DataParallel batch-size — default bs=256 is calibrated for 4x A100
40 GB. Drop to bs=64 for a single GPU or you will OOM inside the
sub-center prototype attention.
Embeddings not persisted — after training writes the checkpoint, the 128-d
projection is re-computed on demand in every downstream analysis; if you want it
cached, re-save the AnnData explicitly via adata.write_h5ad().
Sulic is NOT held out of the standard corpus — all 4,683 Sulic cells
(including the 4,183-cell held_out_labeled slice) are inside
data/corpus/pan_skin/harmonized/corpus.h5ad. Scoring the standard checkpoints on
the Sulic slice is a train-set evaluation. Use
scripts/pan_skin/92_retrain_with_sulic_anchor.py (500-cell anchor corpus,
artefacts 97_sulic_anchor_*) for the honest number, as the paper now does.
Corpus rebuild from raw GEO data is currently not possible from this repo —
the per-dataset loader modules (panda/data/*_loaders.py) that
06_build_per_dataset_h5ads.py / 02_build_per_dataset.py import were never
committed (only stale v2 copies exist under archive/). Everything from
training onward is reproducible from the harmonized corpora on the Hugging Face
mirror; re-deriving the corpora from GEO requires reconstructing the loaders.
12. Artefact index (from PAPER.tex Section 11)
Every quantitative claim traces to one of:
Model checkpoints: checkpoints/{system}/{variant}/panda_final.pt
Marker gene lists: panda/markers.yaml
Corpus builders: scripts/{pan_skin,hematopoiesis,pancreas}/
Dingwall EDEN validation:
Line A discovery/pan_skin/marker/98_eden_summary.json;
Line B data/processed/dingwall_replica/dingwall_replica.h5ad,
replica_cluster_20_qc.json, replica_marker_matches.csv;
Line C discovery/pan_skin/marker/104_dingwall_derm_summary.json + prediction CSVs.
Primary EDEN (Derm2):
discovery/pan_skin/marker/100_primary_eden_discovery.csv,
100_primary_eden_summary.json,
101_derm_identity_summary.json,
101_derm_subcluster_scores.csv.
Central architecture: panda/model.py. Composite loss lives in the same file
(supcon_loss, vicreg_loss, hsic_biased, subcenter_angular_infonce,
prototype_repulsion) and is imported as from panda import PANDAEncoder, ....
Data mirror
Full data (~195 GB corpus + raw + processed + external labels) is mirrored to Hugging Face at bryan7264/PANDA. Fetch with: