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 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.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Space using awatramani-lab/hmoe-da 1