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
pip install hmoe-da # core: numpy, scipy, anndata, scikit-learn
pip install "hmoe-da[io]" # + scanpy, for the CLI / reading .h5ad
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
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.
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 aGad2subtype is reassigned to its best non-Gad2 subtype and flagged inhmoe_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.
predictrefuses input that is not non-negative integers unless you passassume_raw=True. If your counts live in a layer, usepredict(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 aZNF→Zfportholog fallback. Genes missing from your panel are zero-filled; coverage is reported inadata.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 modelv4_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.