--- license: mit library_name: hmoe-da tags: - single-cell - rna-seq - dopaminergic-neurons - cell-type-annotation - neuroscience pipeline_tag: other --- # hmoe-da — open-weights dopaminergic-neuron subtype classifier Assign every cell in a **raw** single-cell / single-nucleus RNA-seq dataset of dopaminergic (DA) neurons to one of **18 mouse-defined subtypes** across three families — **Sox6**, **Calb1**, **Gad2** — using the Hierarchical Mixture-of-Experts (HMoE) model from the **Awatramani Lab, Northwestern University** (Schonfeld et al., manuscript in preparation). This is the exact model used to label human DA neurons in the manuscript (Figure 1, Kamath et al. dataset). The weights are open and bundled in the package (~315 KB); inference is **numpy-only** (no torch, no GPU). Accepts `.h5ad` (AnnData) or Seurat `.rds` input. ## What you get For each cell, written into `adata.obs`: | column | meaning | |---|---| | `hmoe_subtype` | predicted subtype, e.g. `Sox6:Vcan`, `Calb1:Sulf1` | | `hmoe_family` | `Sox6` / `Calb1` / `Gad2` | | `hmoe_confidence` | top subtype probability | | `hmoe_margin` | top1 − top2 probability | | `hmoe_confident` | GMM-based confident-call flag | | `hmoe_gad2_excluded` | cell was a Gad2 call reassigned (only if `include_gad2=False`) | and the full probability matrix in `adata.obsm["X_hmoe_P_sub"]` (n × 18). ## Install ```bash pip install hmoe-da # core: numpy, scipy, anndata, scikit-learn pip install "hmoe-da[io]" # + scanpy, for the CLI / reading .h5ad ``` ## Python ```python import scanpy as sc from hmoe_da import predict adata = sc.read_h5ad("my_da_neurons.h5ad") # RAW COUNTS in .X adata = predict(adata, include_gad2=True) adata.obs["hmoe_subtype"].value_counts() # Seurat .rds also works (needs R + SeuratObject on the system): from hmoe_da import read_adata adata = predict(read_adata("my_da_neurons.rds")) ``` ## Command line ```bash hmoe-da predict my_da_neurons.h5ad -o labeled.h5ad --csv labels.csv hmoe-da predict my_seurat.rds -o labeled.h5ad --csv labels.csv # raw counts elsewhere? --layer counts or --use-raw (.h5ad only) # exclude the Gad2 family? --exclude-gad2 ``` ## Web app (no code) Drag-and-drop an `.h5ad` or Seurat `.rds`, download a labeled CSV: [Hugging Face Space](https://huggingface.co/spaces/awatramani-lab/hmoe-da). Run it locally with `python app.py`. ## Seurat `.rds` input `.rds` files (Seurat, SingleCellExperiment, or a sparse count matrix with gene/cell names) are read by shelling out to your system **R** with **SeuratObject** installed. Raw counts are extracted (`GetAssayData(layer="counts")`, joining v5 layers if needed) into an AnnData. If R is not available, convert to `.h5ad` in R first (`sceasy` or `SeuratDisk`) and use that. ## The Gad2 family option The classifier has a **Gad2** family (GABAergic) alongside Sox6 and Calb1. - `include_gad2=True` (default): `Gad2:*` subtypes are valid predictions. - `include_gad2=False` (**exclude Gad2 family**): any cell whose top call is a `Gad2` subtype is reassigned to its best non-Gad2 subtype and flagged in `hmoe_gad2_excluded`. Use this when you do not expect a GABAergic population — e.g. TH⁺/SLC6A3⁺-sorted dopaminergic data. ## Input requirements (read this) - **Raw counts only.** The gate classifiers were fit on raw UMI counts. Normalized / log-transformed / scaled data yields silently wrong labels. `predict` refuses input that is not non-negative integers unless you pass `assume_raw=True`. If your counts live in a layer, use `predict(adata_with_counts_in_X, ...)`, or the CLI `--layer` / `--use-raw`. - **Gene symbols**, not Ensembl IDs, in `var_names`. Human (UPPER) and mouse (Title) case both work; matching is case-insensitive, with a `ZNF→Zfp` ortholog fallback. Genes missing from your panel are zero-filled; coverage is reported in `adata.uns["hmoe_gene_coverage"]`. ## How it works A soft hierarchical router: at each node a logistic-regression gate softmaxes its children, and probability mass flows down the tree to 18 leaves. Shallow gates use raw counts; deep gates use SCT Pearson residuals computed internally from your raw counts (θ=100). Output is a row-normalized (n × 18) matrix; the per-cell label is its argmax. ## Reproducibility & provenance - `hmoe_da/weights/` — the open weights: `meta.json` (tree + gate structure), `features.json` (21,604-gene alignment axis), `child_models.npz` (34 logreg gates), `MANIFEST.json` (sha256 checksums, source model `v4_improved_unified`). - `scripts/convert_weights.py` — regenerates the bundle from the study repo (the only step needing torch). - `scripts/verify_parity.py` — confirms this package reproduces the manuscript's cached Kamath predictions exactly (labels) and to <1e-5 (P_sub). ## Performance note The SCT residual step materializes a dense cells×genes matrix, so peak memory grows with cell count (fine for a few thousand cells; ~20k cells needs several GB). A future release may chunk this over cells — the residual is computed per-cell independently. ## Citing If you use this classifier, please cite Schonfeld et al. (manuscript in preparation), Awatramani Lab, Northwestern University. © 2026 Awatramani Lab, Northwestern University. Released under the MIT License.