API reference#
Mathematical formulation#
Notation. A mini-batch holds \(B\) cells. The cell branch input is the log1p-normalized expression \(x \in \mathbb{R}^{B \times G}\) over \(G\) genes; the neighborhood branch input is \([\,x \;;\; h\,] \in \mathbb{R}^{B \times 2G}\), the concatenation of a cell’s own expression and its aggregated neighbor expression \(h\). Encoders map these to latents \(z_e^{\text{cell}} \in \mathbb{R}^{B \times d_c}\) and \(z_e^{\text{niche}} \in \mathbb{R}^{B \times d_n}\), each snapped to its codebook. Symbols: \(K\) codebook size, \(D\) code dimension, \(\operatorname{sg}[\cdot]\) the stop-gradient operator.
Total training objective. The trainer minimizes the sum of the two branch
losses, each a reconstruction likelihood plus its VQ (commitment + diversity)
term, weighted by cell_weight and neighborhood_weight (both \(1.0\) by default):
where \(\mathcal{L}_{\text{recon}}^{\text{cell}}\) is the negative-binomial +
detection-hurdle count likelihood, \(\mathcal{L}_{\text{recon}}^{\text{niche}}\) is
the composition MSE + Dirichlet-multinomial term, and each
\(\mathcal{L}_{\text{VQ}}\) is the commitment (+ diversity) loss of its quantizer
(all defined below). Optional spatial-coherence regularizers are added only when
spatial_loss_weight > 0 (off by default).
HierarchicalVQVAE(config)#
Two paired codebooks coupled by cross-attention. The cell encoder produces a cell latent that is quantized to a cell-state code; the neighborhood encoder produces a niche latent that is quantized to a niche code; a one-directional gated cross-attention lets the cell latent read its niche before both branches are decoded and reconstructed.
Parameters
config (ModelConfig) - input dimensionality, codebook sizes (\(K_c, K_n\)), embedding dimensions (\(d_c, d_n\)), encoder / quantizer selection, cross-attention flag and residual weight, and the reconstruction-loss modes. See
ModelConfig.
Shape
Cell input: \((B, G)\) with \(G = \texttt{input\_dim}\).
Neighborhood input: \((B, 2G)\).
Cell reconstruction: \((B, G)\); niche reconstruction: \((B, 2G)\).
Code indices: \((B, 1)\) each; VQ losses and perplexities are scalars.
The quantized cell latent \(z_q^{\text{cell}}\) is conditioned on the quantized niche latent \(z_q^{\text{niche}}\) by a single multi-head attention read, mixed back with a fixed residual weight \(\lambda = \texttt{cross\_attention\_weight}\) (default \(0.5\); this is a constant residual gate, not a learned sigmoid gate):
with \(W_p\) the learned niche-to-cell projection (neighborhood_projection) and the
attention a torch.nn.MultiheadAttention over a single-token sequence per cell
(so the softmax is over one key and reduces to \(V\), i.e. the read returns the
projected niche vector; the head count is the largest divisor of \(d_c\) at most
cross_attention_heads, default \(4\)). This is one-directional: the niche latent is
never updated from the cell (the niche code stays a pure tissue descriptor). The
decoders \(g_{\text{cell}}, g_{\text{niche}}\) then map \(z_q^{\text{cell,final}}\) and
\(z_q^{\text{niche}}\) back to the input spaces.
VectorQuantizer(num_embeddings, embedding_dim, commitment_cost, ...)#
The default quantizer (quantizer_type="vq"). Given an encoded latent it assigns
each row to the nearest of \(K\) codebook entries \(\{e_k\}_{k=1}^{K}\), returns the
quantized vector with straight-through gradients, and refreshes the codebook by
exponential moving average rather than by the optimizer.
Parameters
num_embeddings (int) - codebook size \(K\).
embedding_dim (int) - code dimensionality \(D\).
commitment_cost (float) - commitment weight \(\beta\) (default \(0.25\) via
ModelConfig.commitment_cost).ema_decay (float, default \(0.99\)) - EMA decay \(\gamma\) for the cluster sizes and embedding sums.
diversity_weight (float, default \(1.0\)) - weight of the entropy diversity term; \(0\) disables it.
distance_metric (str, default
"l2") -"l2"(squared Euclidean) or"cosine".
Shape
Input: \((B, D, T)\) channels-first (\(T=1\) in the hierarchical model).
Output: quantized \((B, D, T)\);
encoding_indices\((B\,T, 1)\);loss,perplexityscalars.
Codebook lookup assigns each latent \(z_e\) to the nearest code by squared Euclidean distance:
Because \(\arg\min\) has no gradient, the encoder is trained through a straight-through estimator, \(z_q \leftarrow z_e + \operatorname{sg}[\,z_q - z_e\,]\), which copies the decoder gradient at \(z_q\) back onto \(z_e\). With the EMA codebook the optimizer sees only the commitment term (the codebook is frozen from gradients),
where \(\bar p\) is the batch-mean soft assignment \(\operatorname{softmax}(-\text{dist}/\tau)\) and \(H\) is Shannon entropy (the diversity term pushes usage toward uniform). Without EMA the codebook term \(\lVert z_q - \operatorname{sg}[z_e]\rVert_2^2\) is added instead. The codebook itself is updated by EMA of the per-code assignment counts \(n_k\) and the assigned-vector sums \(m_k = \sum_{i : k^\star_i = k} z_{e,i}\):
with a Laplace smoothing of \(N_k\) (van den Oord 2017; Razavi 2019). Every
dead_code_reset_interval steps, codes whose usage falls below a fraction of their
fair share are reseeded from random batch rows.
mlp_deep encoder (DeepMLPEncoder)#
The default cell / neighborhood backbone (encoder_type="mlp_deep"): a linear input
projection into a residual stream of width \(d = \texttt{hidden}[0]\), a stack of
pre-norm SwiGLU residual blocks, an output LayerNorm, and a linear projection to the
latent. No per-gene numerical embedding (which degenerates on sparse Xenium counts).
Parameters
in_dim, out_dim (int) - input gene count \(G\) (or \(2G\) for the neighborhood branch) and output latent dimension.
hidden (Sequence[int]) - residual-stream width is
hidden[0].dropout (float, default \(0.1\)) - dropout inside the SwiGLU blocks.
n_blocks (int, optional) - number of residual blocks (default \(\max(4, \texttt{len(hidden)})\)).
ffn_mult (float, default \(2.0\)) - SwiGLU hidden expansion factor.
Shape
Input: \((B, \texttt{in\_dim})\). Output: \((B, \texttt{out\_dim})\).
Each block updates the residual stream \(z\) with a pre-norm gated feed-forward:
where \(\odot\) is the elementwise product (the SwiGLU gate) and \(\operatorname{LN}\) is LayerNorm. The output is \(\texttt{proj\_out}(\operatorname{LN}(z))\).
Reconstruction losses#
The default cell branch (cell_recon="nb") is a negative-binomial count
likelihood on the raw integer counts plus a detection hurdle; there is no MSE on the
cell branch. Let \(x \in \mathbb{Z}_{\ge 0}^{B \times G}\) be the raw counts,
\(\ell_i = \sum_g x_{ig}\) the observed library size (scVI convention), \(p =
\operatorname{softmax}(\text{decoder logits})\) the per-gene proportion, and \(\mu = \ell
\odot p\) the NB mean. With the learned per-gene inverse-dispersion \(\theta =
\exp(\texttt{cell\_log\_theta})\), the NB negative log-likelihood is
The detection hurdle is a per-gene Bernoulli / BCE on the \(\mathbf{1}[x>0]\) mask, reusing the decoder output as the detection logits \(\eta\):
Both terms are reduced to per-gene means (divided by \(G\)) so they are comparable, giving the default cell reconstruction loss
with the detection weight \(w_{\text{det}} = \texttt{detection\_weight}\). The "mse"
mode instead recovers the released Gaussian (MSE-on-log1p) term
\(\lVert \hat x - x\rVert_2^2 / (BG)\); "poisson" uses the equidispersed count NLL
(the NB with \(\theta \to \infty\)); "both" sums \(w_{\text{mse}}\,\mathrm{MSE} +
w_{\text{nb}}\,\tfrac{1}{G}\mathrm{NLL}_{\text{NB}}\).
The default niche branch (niche_recon="mse_dirmult") sums a composition MSE on the
log1p niche vector and a Dirichlet-multinomial NLL on the count-scale
aggregated-neighbor composition \(c\) (row sum \(N = \sum_g c_g\)). With the mean
composition \(p = \operatorname{softmax}(\text{logits})\), a learned precision \(\alpha_0
= \sum_g \exp(\texttt{niche\_log\_alpha}_g)\), and Dirichlet parameters \(\alpha = p\,
\alpha_0\), the DM negative log-likelihood is
evaluated with lgamma so fractional \(c\) (the continuous weighted-mean aggregate)
needs no rounding. The niche loss is \(w_{\text{mse}}\,\mathrm{MSE} + w_{\text{dm}}\,
\tfrac{1}{G}\mathrm{NLL}_{\text{DM}}\) (both weights default \(1.0\)).
MoleculeSetVQVAE(...)#
The molecule-set variant of HierarchicalVQVAE: the cell branch
is a MoleculeSetEncoder over a cell’s transcript point
cloud (one token per molecule, \(\text{token} = E_{\text{gene}}(g) +
\operatorname{MLP}(\operatorname{Fourier}(dx, dy))\)), pooled by
\(\operatorname{concat}[\text{masked max},\ \text{masked mean},\ \text{PMA seed}]\)
(never PMA alone, which collapses the pooled embedding). It feeds the same cell
codebook, an optional aggregated-kNN neighborhood branch, and the same gated
cross-attention, so the emitted cell / niche code indices match the standard model.
Parameters
n_genes (int, default \(366\)) - panel size (also the padding gene index).
cell_embedding_dim, cell_num_embeddings (int) - cell code dimension \(d_c\) and count \(K_c\) (defaults \(64\), \(256\)).
neighborhood_embedding_dim, neighborhood_num_embeddings (int) - niche code dimension \(d_n\) and count \(K_n\) (defaults \(256\), \(32\)).
commitment_cost (float, default \(0.25\)) - VQ commitment weight \(\beta\).
use_neighborhood, use_cross_attention (bool, default
True) - enable the niche branch and the cell-reads-niche cross-attention.cross_attention_weight (float, default \(0.5\)) - residual mix \(\lambda\).
enc_width, enc_isab, enc_heads, enc_inds, enc_freqs - Set-Transformer encoder width, number of ISAB blocks, attention heads, induced points, and Fourier frequencies.
Shape
Molecule inputs:
gene\((B, M)\) long,coords\((B, M, 2)\) microns,mask\((B, M)\) bool; \(M\) is the padded molecule count. Neighborhood features (optional): \((B, 2G)\).Cell reconstruction: \((B, G)\); code indices \((B, 1)\) each.
ModelConfig defaults (b32k production model)#
The released RCC/BrM production model (b32k) is reproduced by the
ModelConfig defaults below. Where a field’s dataclass default
differs from a legacy checkpoint’s serialized value, the code default is authoritative
(the loader falls back to the pre-refactor cell_recon="default" / niche_recon="mse"
/ detection_weight=0.0 only for checkpoints saved before those fields existed).
Field |
Default |
Meaning |
|---|---|---|
|
\(256 \times 64\) |
cell codebook \(K_c \times d_c\) |
|
\(32 \times 256\) |
niche codebook \(K_n \times d_n\) |
|
\(0.25\) |
VQ commitment weight \(\beta\) |
|
|
SwiGLU pre-norm residual MLP |
|
|
EMA codebook (default |
|
|
squared-Euclidean code assignment |
|
|
cell reads niche |
|
\(0.5\) |
residual mix \(\lambda\) |
|
\(4\) |
attention heads |
|
|
NB count likelihood + detection hurdle |
|
\(0.5\) |
hurdle weight \(w_{\text{det}}\) |
|
|
composition MSE + Dirichlet-multinomial |
|
\(1.0\), \(1.0\) |
niche-loss weights |
|
\((256, 128)\) |
encoder MLP widths (decoder reversed) |
TrainConfig defaults (b32k trajectory)#
The released training trajectory is reproduced by the TrainConfig
defaults below.
Field |
Default |
Meaning |
|---|---|---|
|
\(300\) |
full passes over the cohort |
|
\(32768\) |
cells per mini-batch (b32k) |
|
\(3\times10^{-4}\) |
Adam base LR (plateau-scheduled) |
|
\(0.01\) |
decoupled AdamW (weights only, codebooks excluded) |
|
|
k-NN capped at |
|
\(50.0\) |
niche-edge cap in microns |
|
\(20\) |
neighbors per cell |
|
|
inverse-distance neighbor pooling |
|
\(1.0\), \(1.0\) |
branch multipliers \(w_{\text{cell}}, w_{\text{niche}}\) |
|
|
halve LR on val/train plateau (patience \(5\)) |
|
\(0.0\) |
spatial-coherence regularizer off by default |
Model#
The hierarchical VQ-VAE: a per-cell encoder, a cell codebook, a neighborhood codebook, and the cross-attention that couples them.
- class nicheverse.ModelConfig(input_dim, hidden_dims=(256, 128), cell_embedding_dim=64, cell_num_embeddings=256, neighborhood_embedding_dim=256, neighborhood_num_embeddings=32, commitment_cost=0.25, use_cross_attention=True, cross_attention_weight=0.5, cross_attention_heads=4, vq_distance='l2', quantizer_type='vq', quantizer_kwargs=<factory>, encoder_type='mlp_deep', encoder_kwargs=<factory>, recon='mse', cell_recon='nb', detection_weight=0.5, w_mse=1.0, w_nb=1.0, niche_recon='mse_dirmult', w_niche_mse=1.0, w_dirmult=1.0, gene_names=<factory>)#
Bases:
objectConfiguration for
HierarchicalVQVAE.The default field values reproduce the published RCC/BrM production model: a cell codebook (256 entries, 64 dims) and a neighborhood codebook (32 entries, 256 dims), coupled by multi-head cross-attention.
- Parameters:
input_dim (
int) – Number of genes in the input panel.hidden_dims (
tuple[int,...], default:(256, 128)) – Width of each hidden layer of the encoder MLP. The decoder uses the reversed sequence. Must have length >= 1.cell_embedding_dim (
int, default:64) – Dimensionality of the cell latent space and cell codebook entries.cell_num_embeddings (
int, default:256) – Number of cell codebook entries (cell states).neighborhood_embedding_dim (
int, default:256) – Dimensionality of the neighborhood latent space and codebook entries.neighborhood_num_embeddings (
int, default:32) – Number of neighborhood codebook entries (niches).commitment_cost (
float, default:0.25) – Weight of the VQ commitment loss term (van den Oord 2017, beta).use_cross_attention (
bool, default:True) – If True, condition the cell representation on its niche representation via a multi-head cross-attention block before reconstruction.cross_attention_weight (
float, default:0.5) – Residual mixing weight for cross-attention output (default 0.5).cross_attention_heads (
int, default:4) – Number of attention heads (default 4). Automatically reduced to the largest divisor ofcell_embedding_dimthat is<=this value when the two are incompatible, so any embedding dimension is accepted.vq_distance (
str, default:'l2') – Codebook assignment metric:"l2"(default, squared Euclidean) or"cosine"(assign to the maximum-cosine-similarity code).quantizer_type (
str, default:'vq') – Codebook family ("vq"default; alsofsq/soft/rot/qinco/pq) and its extra keyword arguments. Seebuild_quantizer().quantizer_kwargs (
dict[str,Any], default:<factory>) – Codebook family ("vq"default; alsofsq/soft/rot/qinco/pq) and its extra keyword arguments. Seebuild_quantizer().encoder_type (
str, default:'mlp_deep') – Encoder backbone ("mlp_deep"default; alsomlp/mlp_plr/residual_mlp/transformer) and its extra keyword arguments. The default ismlp_deep(a SwiGLU pre-norm residual MLP, no per-gene numerical embedding): it had the best raw codebook health in the RCC Xenium sweep, and per-gene numerical embeddings (mlp_plr/ PLE) degenerate on the very sparse Xenium counts that are the default sweep data. Seebuild_encoder().encoder_kwargs (
dict[str,Any], default:<factory>) – Encoder backbone ("mlp_deep"default; alsomlp/mlp_plr/residual_mlp/transformer) and its extra keyword arguments. The default ismlp_deep(a SwiGLU pre-norm residual MLP, no per-gene numerical embedding): it had the best raw codebook health in the RCC Xenium sweep, and per-gene numerical embeddings (mlp_plr/ PLE) degenerate on the very sparse Xenium counts that are the default sweep data. Seebuild_encoder().recon (
str, default:'mse') – Reconstruction likelihood:"mse"(default, Gaussian),"nb"(negative binomial, raw counts), or"poisson"(raw counts).cell_recon (
str, default:'nb') –Cell-branch reconstruction mode. The default is now ``”nb”``, a negative-binomial count likelihood on the RAW integer counts PLUS a Bernoulli detection hurdle:
cell_loss = NB_NLL(raw counts) + detection_weight * BCE(detection)The NB NLL uses a softmax-proportion decoder scaled by the OBSERVED total count per cell (the standard scVI library) and allocates a learned per-gene dispersion
cell_log_theta; BCE is a hurdle on the 0-vs-nonzero mask of the raw counts (the decoder output is reused as the detection logits). There is NO MSE term on the cell branch, so there is no log-space-vs-count-space scale mixing; only the NB and BCE terms need balancing, and both are reduced to per-gene means (divided byinput_dim) so they are comparable, then BCE is weighted bydetection_weight. The default needs raw INTEGER counts as the NB / detection target, captured before normalize/log1p; the default training pipeline provides them viaTrainConfig(normalize=True, log1p=True)(log1p expression is the encoder input, raw counts stashed to a layer).The remaining modes stay selectable so the old behavior is fully recoverable:
"mse"= pure MSE-on-log1p (withniche_recon="mse"anddetection_weight=0this is the exact released MSE-only path);"poisson"= a pure Poisson count NLL on the raw counts;"both"= MSE-on-log1p + NB (each term reduced so neither dominates), balanced byw_mse/w_nb;"default"= defer toreconon the encoder input directly (byte-identical to the pre-composite path). For every count mode (nb/poisson/both), the detection hurdle is added wheneverdetection_weight > 0.detection_weight (
float, default:0.5) – Weight of the Bernoulli/BCE detection-hurdle term (per-gene-mean BCE on the 0-vs-nonzero mask of the raw counts, decoder output reused as the detection logits). Default ``0.5`` (on, so the default"nb"mode is NB + BCE). Applied for any count mode (nb/poisson/both); IGNORED for"mse"/"default"(no raw-count target). Set to0to drop it.w_mse (
float, default:1.0) – Balancing weights for the MSE-on-log1p and NB-NLL terms of the"both"cell mode (unused by the default"nb"mode, which has no MSE term). Both default to1.0. The NB term in"both"is reduced to a per-gene mean so it is comparable to the per-element MSE at unit weight.w_nb (
float, default:1.0) – Balancing weights for the MSE-on-log1p and NB-NLL terms of the"both"cell mode (unused by the default"nb"mode, which has no MSE term). Both default to1.0. The NB term in"both"is reduced to a per-gene mean so it is comparable to the per-element MSE at unit weight.niche_recon (
str, default:'mse_dirmult') – Neighborhood-branch reconstruction mode. The default is now ``”mse_dirmult”``, a balanced sum of the composition MSE on the log1p niche vector and a Dirichlet-multinomial NLL whose target is the COUNT-SCALE aggregated-neighbor composition (the SAME weighted-mean neighbor aggregation applied to the RAW counts, so its row sum is a real transcript total, NOT a log1p mean):niche_loss = w_niche_mse * MSE(log1p niche) + w_dirmult * DirMult_NLL(count-scale comp), with the DirMult reduced to a per-feature mean so it is comparable to the MSE."mse"(pure released composition MSE) and"dirichlet_multinomial"(pure count-scale DirMult on the neighbor half, log1p MSE on the self half) stay selectable. The DM niche modes require raw integer counts (so the count-scale neighbor composition can be built), i.e. a countcell_recon; otherwise the trainer raises a clear error.w_niche_mse (
float, default:1.0) – Balancing weights for the two terms of the composite niche loss ("mse_dirmult"). Both default to1.0; the DirMult term is a per-feature mean so it is comparable to the composition MSE at unit weight.w_dirmult (
float, default:1.0) – Balancing weights for the two terms of the composite niche loss ("mse_dirmult"). Both default to1.0; the DirMult term is a per-feature mean so it is comparable to the composition MSE at unit weight.gene_names (
tuple[str,...], default:<factory>) – Tuple of gene names matchinginput_dim. Recorded in the checkpoint so thatnicheverse.predict_codes()can verify gene panel compatibility at inference time.
- Raises:
ValueError – If
hidden_dimsis empty, any dimension is non-positive, orvq_distanceis not one of{"l2", "cosine"}.
- classmethod from_dict(d)#
Build a
ModelConfigfrom a JSON-loaded dict.Tolerates bytes in
gene_names(which can come out of h5 attribute load paths) by decoding them to str.- Parameters:
- Return type:
- class nicheverse.HierarchicalVQVAE(config)#
Bases:
ModuleTwo-codebook hierarchical VQ-VAE for spatial transcriptomics.
- Parameters:
config (
ModelConfig) –ModelConfiginstance with input dimensionality, codebook sizes, and cross-attention flag.
Notes
The cross-attention block (when enabled) gives the cell representation access to the niche representation via a single multi-head attention call:
q_cell_final = q_cell + cross_attention_weight * Attention(q_cell, proj(q_neigh), proj(q_neigh))The residual weight is exposed as
config.cross_attention_weight(default 0.5) so it can be ablated or tuned without code changes.- forward(cell_features, neighborhood_features)#
Run the full forward pass.
- Parameters:
- Return type:
tuple[Tensor,Tensor,Tensor,Tensor,Tensor,Tensor,Tensor,Tensor]- Returns:
cell_recon, neigh_recon – Reconstructions of the inputs.
cell_vq_loss, neigh_vq_loss – Scalar VQ losses (commitment plus diversity, plus codebook MSE if EMA is disabled).
cell_idx, neigh_idx –
(B, 1)hard code assignments.cell_perp, neigh_perp – Batch perplexities of the two codebooks.
- encode(cell_features, neighborhood_features)#
Return
(cell_idx, neighborhood_idx)code assignments without reconstruction.A lightweight inference helper for annotating cells: it skips the decoders and returns the two hard code-index vectors of shape
(B,).
- classmethod from_checkpoint(path, device='cpu')#
Load a trained model from a checkpoint written by
save_checkpoint().- Parameters:
- Return type:
- nicheverse.save_checkpoint(model, path)#
Save model state dict and embedded
ModelConfigtopath.Writes
path(a.ptfile) and a sibling.jsonof the config for human inspection.- Returns:
The checkpoint path actually written.
- Return type:
Path
- Parameters:
model (
HierarchicalVQVAE)
- nicheverse.load_checkpoint(path, device='cpu', config=None)#
Load a checkpoint saved by
save_checkpoint().- Parameters:
device (
str|device, default:'cpu') – Target device for the loaded model.config (
Optional[ModelConfig], default:None) – Required only for legacy bare-state-dict checkpoints with no embedded config. New checkpoints carry the config and ignore this argument.
- Raises:
FileNotFoundError – If
pathdoes not exist.ValueError – If the checkpoint is a legacy bare state dict and
configis None.
- Return type:
Quantizers#
Each quantizer maps a continuous embedding to a discrete code. vq is the
released default; the others are swappable through ModelConfig.quantizer_type
and the registry.
- class nicheverse.VectorQuantizer(num_embeddings, embedding_dim, commitment_cost, use_ema=True, ema_decay=0.99, diversity_weight=1.0, diversity_temperature=1.0, dead_code_reset_interval=50, dead_code_usage_fraction=0.01, distance_metric='l2')#
Bases:
ModuleEMA vector quantizer with k-means++ init, dead code reset, and diversity term.
This is the default quantizer (
quantizer_type="vq") and the discretization core of the NICHEVERSE model. Given an encoded cell (or neighborhood) embedding it snaps that continuous vector to the nearest ofKlearned codebook entries, so the integer code index becomes the cell-state (or niche) vocabulary symbol that downstream annotation and the neighborhood codebook build on. Gradients flow to the encoder through a straight-through estimator, while the codebook itself is updated by exponential moving averages rather than by the optimizer.The forward pass takes a tensor of shape
(B, D, T)(channels-first, following the VQ-VAE convention), flattens to(B*T, D), finds the nearest codebook entry for each row by squared Euclidean distance, and returns the quantized output with straight-through gradients.- Parameters:
num_embeddings (
int) – Codebook sizeK.embedding_dim (
int) – Codebook entry dimensionalityD.commitment_cost (
float) – Weightbetaof the commitment lossbeta * ||sg(e) - z||^2.use_ema (
bool, default:True) – If True, update codebook entries with EMA rather than gradient descent (recommended; matches Razavi et al. 2019).ema_decay (
float, default:0.99) – EMA decay factorgammafor cluster sizes and embedding sums.diversity_weight (
float, default:1.0) – Weight of the entropy maximizing diversity term. Set to 0 to disable.diversity_temperature (
float, default:1.0) – Softmax temperature applied to negative distances before computing the batch average soft assignment used in the diversity term. Lower values sharpen the soft assignment (closer to the hard one).dead_code_reset_interval (
int, default:50) – Number of training steps between dead code resets.dead_code_usage_fraction (
float, default:0.01) – A code is considered dead if its EMA usage (an EMA of raw per-code assignment counts) falls below this fraction of the per-code fair share, i.e. belowdead_code_usage_fraction * batch_size / num_embeddings. The fair-share normalization matters: without it the threshold would exceed the uniform steady state and flag every code dead each interval.distance_metric (
str, default:'l2') – Assignment metric,"l2"(default, squared Euclidean) or"cosine"(assign to the maximum-cosine-similarity code). Must be one of those two.
- embedding#
The codebook, weight shape
(K, D)=(num_embeddings, embedding_dim). Rowiis codei’s prototype vector.- Type:
- _initialized#
Bool buffer, shape
(); flipped toTrueafter the first-batch k-means++ seeding so init runs exactly once.- Type:
- ema_cluster_size#
EMA of per-code assignment counts, shape
(K,)(only whenuse_ema).- Type:
- ema_embed_sum#
EMA of the sum of assigned vectors per code, shape
(K, D)(only whenuse_ema); divided byema_cluster_sizeto refreshembedding.- Type:
- code_usage#
Slower EMA of per-code usage, shape
(K,), used to detect dead codes.- Type:
- update_count#
Long buffer, shape
(); counts EMA updates to schedule dead-code resets.- Type:
Notes
The codebook entries are initialized with k-means++ on the first training batch (van den Oord 2017 used uniform init but k-means++ converges faster and reduces dead codes early in training). When the batch is smaller than
num_embeddingswe still seed every slot: the firstmin(B*T, num_embeddings)slots are filled from the batch, the remainder are seeded by sampling with replacement so all slots start from data.During training the codebook is refreshed by EMA of the cluster sizes and assigned-vector sums (van den Oord 2017; Razavi et al. 2019), so codebook entries do not receive optimizer gradients. Encoder gradients bypass the non-differentiable argmax through a straight-through estimator (
quantized = inputs + (quantized - inputs).detach()). Everydead_code_reset_intervalupdates, codes whose usage fell below threshold are reseeded from random batch rows to keep the codebook fully utilized. The optional diversity term addsdiversity_weight * (log K - H(p))wherepis the batch-mean soft assignment, pushing the codebook toward balanced use.- extra_repr()#
Set the extra representation of the module.
To print customized extra information, you should re-implement this method in your own modules. Both single-line and multi-line strings are acceptable.
- Return type:
- forward(inputs)#
Quantize
inputsagainst the learned codebook.- Parameters:
inputs (
Tensor) – Tensor of shape(B, D, T)(channels-first). The hierarchical model usesT=1.- Return type:
- Returns:
loss – Scalar VQ loss (commitment + codebook MSE if not using EMA + diversity).
quantized – Tensor of shape
(B, D, T); carries straight-through gradients.perplexity – Scalar perplexity
exp(H(p))wherepis the batch-average hard-assignment distribution.encoding_indices – Tensor of shape
(B*T, 1)with the assigned code index per row.Shape
—–
- Input (
(B, D, T)withD = embedding_dim,K = num_embeddings.)- Output (
loss(),quantized(B, D, T),perplexity) –(),encoding_indices(B*T, 1).
- class nicheverse.models.FSQ(embedding_dim, levels=(8, 5, 5, 5), commitment_cost=0.0, diversity_weight=0.0, var_target=1.0)#
Bases:
ModuleFinite Scalar Quantization (Mentzer et al. 2023): codebook-free discretization.
Projects the latent to
len(levels)scalar dims, bounds each with a shiftedtanh(using the canonical even-level offset so every level yields that many distinct integer codes), rounds with a straight-through estimator, and encodes a single integer index via mixed-radix. Effective code count isprod(levels); the index is a bijection onto[0, prod(levels)).- Parameters:
embedding_dim (
int) – Latent dimensionality; projected to and fromlen(levels).levels (
tuple[int,...], default:(8, 5, 5, 5)) – Per-dimension quantization levels (each >= 3; level 2 is degenerate).commitment_cost (
float, default:0.0) – Accepted for a uniform interface but unused: FSQ has no codebook to commit to; the straight-through round ties the latent to the reconstruction.diversity_weight (
float, default:0.0) – Optional differentiable range-usage penaltydiversity_weight * relu(var_target - var(bounded_z))(0 disables it; FSQ is collapse-resistant by construction).var_target (
float, default:1.0) – Optional differentiable range-usage penaltydiversity_weight * relu(var_target - var(bounded_z))(0 disables it; FSQ is collapse-resistant by construction).
Notes
Forward takes
(B, D, T)and returns(loss, quantized (B, D, T), perplexity, index (B*T, 1)).- property embedding: Embedding#
Materialize the implicit FSQ codebook (grid points mapped through
project_out).
- forward(inputs)#
Project to scalar dims, bound and round (straight-through), and encode a mixed-radix index.
The latent is projected to
len(levels)scalar channels, each bounded by the shiftedtanhand rounded to its integer level with a straight-through estimator, then decoded back to the latent space. The per-dimension integers are packed into one index in[0, prod(levels))by the mixed-radix_basis. There is no learned codebook, so the loss is zero unless the optional range-usage penalty is enabled.- Parameters:
inputs (torch.Tensor) – Encoded latent, shape
(B, D, T)(channels-first; the model usesT=1), withD = embedding_dim.- Return type:
- Returns:
loss (torch.Tensor) – Scalar loss;
0unlessdiversity_weight > 0adds the range-usage penalty.quantized (torch.Tensor) – Straight-through quantized latent, shape
(B, D, T).perplexity (torch.Tensor) – Scalar perplexity of the batch code-usage histogram.
encoding_indices (torch.Tensor) – Mixed-radix code index per row, shape
(B*T, 1), in[0, prod(levels)).Shape
—–
- Input (
(B, D, T).)- Output (
loss(),quantized(B, D, T),perplexity) –(),encoding_indices(B*T, 1).
- class nicheverse.models.SoftVQ(num_embeddings, embedding_dim, commitment_cost=0.25, temperature=0.07, diversity_weight=0.01)#
Bases:
ModuleSoft vector quantizer: differentiable softmax assignment over a learnable codebook.
- Parameters:
num_embeddings (
int) – Codebook sizeKand entry dimensionalityD.embedding_dim (
int) – Codebook sizeKand entry dimensionalityD.commitment_cost (
float, default:0.25) – Weight of the soft commitment MSE (quantizedtoward the input).temperature (
float, default:0.07) – Softmax temperature on the cosine-similarity logits (lower is sharper).diversity_weight (
float, default:0.01) – Weight of the marginal-entropy load-balancing term (0 disables it).
Notes
quantized = softmax(cos_sim / temperature) @ codebookis differentiable everywhere (no straight-through estimator); the hardargmaxindex is returned for annotation. Forward takes(B, D, T)and returns(loss, quantized (B, D, T), perplexity, index (B*T, 1)).- forward(inputs)#
Softly assign the input to the codebook by temperature-scaled cosine similarity.
Returns
(loss, quantized, perplexity, index)wherequantizedis the softmax-weighted codebook average (differentiable, no straight-through) andindexis the hard argmax code for annotation.Shape#
Input:
(B, D, T).Output:
loss(),quantized(B, D, T),perplexity(),index(B*T, 1).
- param inputs:
- type inputs:
- class nicheverse.models.RotVQ(num_embeddings, embedding_dim, commitment_cost=0.25, num_householders=8, distance_metric='l2')#
Bases:
_VQDelegate,ModuleVQ in a learned rotated basis (product of Householder reflections).
Applies an exactly-orthogonal learned rotation to the latent, quantizes with a standard EMA
VectorQuantizerin that basis, then applies the exact inverse rotation to the quantized vector. Decorrelates axes to raise codebook utilization; the per-cell code is a single integer.embedding/num_embeddings/embedding_dim/distance_metricdelegate to the wrapped quantizer so it satisfies the same contract asVectorQuantizer.- Parameters:
num_embeddings (
int) – Codebook size and entry dimensionality of the wrapped EMA quantizer.embedding_dim (
int) – Codebook size and entry dimensionality of the wrapped EMA quantizer.commitment_cost (
float, default:0.25) – Passed through to the wrappedVectorQuantizer.distance_metric (
str, default:'l2') – Passed through to the wrappedVectorQuantizer.num_householders (
int, default:8) – Number of Householder reflections composing the rotation.
- forward(inputs)#
Rotate into the learned basis, quantize there, and rotate the result back.
The input is flattened and passed through the composed Householder reflections, the rotated latent is quantized by the wrapped EMA
VectorQuantizer(which owns the loss, perplexity, index, and straight-through gradient), and the quantized vector is mapped back with the exact inverse rotation. The code index and loss are those of the wrapped quantizer, so the contract matchesVectorQuantizer.- Parameters:
inputs (torch.Tensor) – Encoded latent, shape
(B, D, T)(channels-first; the model usesT=1), withD = embedding_dim.- Return type:
- Returns:
loss (torch.Tensor) – Scalar VQ loss from the wrapped quantizer.
quantized (torch.Tensor) – Inverse-rotated quantized latent, shape
(B, D, T); carries straight-through gradients through the wrapped quantizer.perplexity (torch.Tensor) – Scalar codebook perplexity of the wrapped quantizer.
encoding_indices (torch.Tensor) – Assigned code index per row, shape
(B*T, 1).Shape
—–
- Input (
(B, D, T).)- Output (
loss(),quantized(B, D, T),perplexity) –(),encoding_indices(B*T, 1).
- class nicheverse.models.QINCoVQ(num_embeddings, embedding_dim, commitment_cost=0.25, num_levels=4, distance_metric='l2')#
Bases:
_VQDelegate,ModuleQINCo-style conditional residual VQ (Huijben et al. 2024, arXiv:2401.14732).
A stack of
num_levelscodebooks applied coarse-to-fine on the residual. Level 0 is a plain EMAVectorQuantizer; each deeper level quantizes a query produced by a small MLP conditioned on the residual and the running sum of quantized vectors, so later codes refine earlier ones. The returned single-integer code is the level-0 (coarse) index, which keepscell_codebook_idxa stable single column; the quantized output is the sum over all levels.embedding/num_embeddings/embedding_dim/distance_metricdelegate to level 0.- Parameters:
num_embeddings (
int) – Per-level codebook size and entry dimensionality.embedding_dim (
int) – Per-level codebook size and entry dimensionality.commitment_cost (
float, default:0.25) – Passed to each level’sVectorQuantizer.distance_metric (
str, default:'l2') – Passed to each level’sVectorQuantizer.num_levels (
int, default:4) – Number of residual levels (>= 1).
- forward(inputs)#
Quantize coarse-to-fine over the residual and sum the per-level quantized vectors.
Level 0 quantizes the input with a plain EMA
VectorQuantizer. Each deeper level quantizes a query formed by an MLP conditioned on the running residual and the accumulated quantized sum, so later codes refine earlier ones (the residual is detached before subtracting each level’s output). The output is the sum over all levels; the returned single-integer code and perplexity are level 0’s (coarse) values, keepingcell_codebook_idxa stable single column.- Parameters:
inputs (torch.Tensor) – Encoded latent, shape
(B, D, T)(channels-first; the model usesT=1), withD = embedding_dim.- Return type:
- Returns:
loss (torch.Tensor) – Scalar sum of the per-level VQ losses.
quantized (torch.Tensor) – Sum of the per-level quantized vectors, shape
(B, D, T).perplexity (torch.Tensor) – Scalar perplexity of the level-0 codebook.
encoding_indices (torch.Tensor) – Level-0 (coarse) code index per row, shape
(B*T, 1).Shape
—–
- Input (
(B, D, T).)- Output (
loss(),quantized(B, D, T),perplexity) –(),encoding_indices(B*T, 1).
- class nicheverse.models.ProductVQ(num_embeddings, embedding_dim, commitment_cost=0.25, num_subspaces=4, distance_metric='l2')#
Bases:
_VQDelegate,ModuleProduct quantization: split the latent into
num_subspaceschunks, each quantized by its own codebook.Effective code count is
num_embeddings ** num_subspacesat onlynum_embeddings * embedding_dimparameters, which resists collapse and factorizes a cell into independent programs. The returned single-integer code is subspace 0’s index (kept for a stablecell_codebook_idx); the quantized output is the concatenation over subspaces.embedding/num_embeddings/embedding_dim/distance_metricdelegate to subspace 0 (soembedding_dimreports the per-subspace width).- Parameters:
num_embeddings (
int) – Codebook size per subspace and total latent dimensionality (must be divisible bynum_subspaces).embedding_dim (
int) – Codebook size per subspace and total latent dimensionality (must be divisible bynum_subspaces).commitment_cost (
float, default:0.25) – Passed to each subspace’sVectorQuantizer.distance_metric (
str, default:'l2') – Passed to each subspace’sVectorQuantizer.num_subspaces (
int, default:4) – Number of subspaces the latent is split into.
- forward(inputs)#
Split the latent into subspaces, quantize each independently, and concatenate.
The channel dimension is split into
num_subspacesequal chunks, each quantized by its own EMAVectorQuantizer, and the quantized chunks are concatenated back to the full width. The loss is the sum over subspaces; the returned single-integer code and perplexity are subspace 0’s, keepingcell_codebook_idxa stable single column while the effective code count isnum_embeddings ** num_subspaces.- Parameters:
inputs (torch.Tensor) – Encoded latent, shape
(B, D, T)(channels-first; the model usesT=1), withDthe fullembedding_dimdivisible bynum_subspaces.- Return type:
- Returns:
loss (torch.Tensor) – Scalar sum of the per-subspace VQ losses.
quantized (torch.Tensor) – Concatenation of the per-subspace quantized latents, shape
(B, D, T).perplexity (torch.Tensor) – Scalar perplexity of subspace 0’s codebook.
encoding_indices (torch.Tensor) – Subspace-0 code index per row, shape
(B*T, 1).Shape
—–
- Input (
(B, D, T).)- Output (
loss(),quantized(B, D, T),perplexity) –(),encoding_indices(B*T, 1).
- class nicheverse.models.ResidualVQ(num_embeddings, embedding_dim, commitment_cost=0.25, num_stages=4, distance_metric='l2')#
Bases:
_VQDelegate,ModuleResidual vector quantization: a stack of EMA
VectorQuantizerstages.Stage 0 quantizes the input; each later stage quantizes the residual left by the running sum of quantized vectors (the residual is detached before subtracting each stage’s output, exactly as in
QINCoVQ), so deeper stages refine coarser ones. The output is the sum over stages; the returned single-integer code and perplexity are stage 0’s (coarse) values, keepingcell_codebook_idxa stable single column.embedding/num_embeddings/embedding_dim/distance_metricdelegate to stage 0. UnlikeQINCoVQthere is no conditioning MLP: each stage is a plain residual codebook.- Parameters:
num_embeddings (
int) – Per-stage codebook size and entry dimensionality.embedding_dim (
int) – Per-stage codebook size and entry dimensionality.commitment_cost (
float, default:0.25) – Passed to each stage’sVectorQuantizer.distance_metric (
str, default:'l2') – Passed to each stage’sVectorQuantizer.num_stages (
int, default:4) – Number of residual stages (>= 1).
- forward(inputs)#
Quantize the residual stage by stage and sum the per-stage quantized vectors.
- Parameters:
inputs (torch.Tensor) – Encoded latent, shape
(B, D, T)(channels-first; the model usesT=1), withD = embedding_dim.- Return type:
- Returns:
loss (torch.Tensor) – Scalar sum of the per-stage VQ losses.
quantized (torch.Tensor) – Sum of the per-stage quantized vectors, shape
(B, D, T); carries straight-through gradients.perplexity (torch.Tensor) – Scalar perplexity of stage 0’s codebook.
encoding_indices (torch.Tensor) – Stage-0 (coarse) code index per row, shape
(B*T, 1).Shape
—–
- Input (
(B, D, T).)- Output (
loss(),quantized(B, D, T),perplexity) –(),encoding_indices(B*T, 1).
- class nicheverse.models.LFQ(embedding_dim, commitment_cost=0.25, entropy_weight=0.1, commitment_weight=None)#
Bases:
ModuleLookup-Free Quantization (Yu et al., MAGVIT-v2, ICLR 2024, arXiv:2310.05737).
There is no learned codebook: each latent dimension becomes one bit via its sign, so the implicit codebook is
{-1, +1}**embedding_dimand the effective code count is2**embedding_dim. The integer code is the bit-packed sign pattern. A commitment term ties the latent to its binarization and an entropy term (marginal minus conditional bit entropy) prevents dead bits.- Parameters:
embedding_dim (
int) – Latent dimensionality; also the number of bits (code count2**embedding_dim).commitment_cost (
float, default:0.25) – Default weight of the commitment term (used whencommitment_weightis None).entropy_weight (
float, default:0.1) – Weight of the bit-entropy regularizer.commitment_weight (
Optional[float], default:None) – Explicit commitment weight; falls back tocommitment_costwhen None.
Notes
embedding_dimis the working (bit) dimensionality andnum_embeddingsis the nominal code count2**embedding_dim. To stay within int64, the packed index uses at most 62 bits (so the index is exact forembedding_dim <= 62and always in[0, num_embeddings)). Forward takes(B, D, T)and returns(loss, quantized (B, D, T), perplexity, index (B*T, 1)).- forward(inputs)#
Binarize each latent dimension by its sign and pack the bits into one integer code.
- Parameters:
inputs (torch.Tensor) – Encoded latent, shape
(B, D, T)(channels-first; the model usesT=1), withD = embedding_dim.- Return type:
- Returns:
loss (torch.Tensor) – Scalar commitment + entropy loss.
quantized (torch.Tensor) – Straight-through binarized latent (
+/-1per dim), shape(B, D, T).perplexity (torch.Tensor) – Scalar perplexity of the (capped) batch code histogram.
encoding_indices (torch.Tensor) – Bit-packed code index per row, shape
(B*T, 1), in[0, 2**D).Shape
—–
- Input (
(B, D, T).)- Output (
loss(),quantized(B, D, T),perplexity) –(),encoding_indices(B*T, 1).
- class nicheverse.models.BSQ(embedding_dim, commitment_cost=0.25, gamma=1.0)#
Bases:
ModuleBinary Spherical Quantization (Zhao et al., ICML 2024, arXiv:2406.07548).
The latent is L2-normalized onto the unit sphere and then binarized per dimension to
+/- 1/sqrt(D), so codes lie on hypercube vertices on the sphere and the effective code count is2**embedding_dim. The integer code is the bit-packed sign pattern. A marginal bit-entropy term encourages balanced bit usage.- Parameters:
embedding_dim (
int) – Latent dimensionality; also the number of bits (code count2**embedding_dim).commitment_cost (
float, default:0.25) – Accepted for a uniform interface; BSQ’s loss is the entropy regularizer only.gamma (
float, default:1.0) – Temperature of the soft-sign used in the bit-entropy regularizer.
Notes
embedding_dimis the working (bit) dimensionality andnum_embeddingsis the nominal code count2**embedding_dim; the packed index uses at most 62 bits so it stays in[0, num_embeddings)within int64. Forward takes(B, D, T)and returns(loss, quantized (B, D, T), perplexity, index (B*T, 1)).- forward(inputs)#
Project onto the unit sphere, binarize each dimension, and pack the bits into a code.
- Parameters:
inputs (torch.Tensor) – Encoded latent, shape
(B, D, T)(channels-first; the model usesT=1), withD = embedding_dim.- Return type:
- Returns:
loss (torch.Tensor) – Scalar marginal bit-entropy regularizer.
quantized (torch.Tensor) – Straight-through spherical binarized latent, shape
(B, D, T).perplexity (torch.Tensor) – Scalar perplexity of the (capped) batch code histogram.
encoding_indices (torch.Tensor) – Bit-packed code index per row, shape
(B*T, 1), in[0, 2**D).Shape
—–
- Input (
(B, D, T).)- Output (
loss(),quantized(B, D, T),perplexity) –(),encoding_indices(B*T, 1).
- class nicheverse.models.ResidualFSQ(embedding_dim, levels=(8, 5, 5, 5), num_stages=3, commitment_cost=0.0, diversity_weight=0.0)#
Bases:
ModuleResidual finite scalar quantization (arXiv:2508.15860): multi-stage
FSQ.Stacks
num_stagescodebook-freeFSQlayers, each quantizing the residual left by the previous stages (residual detached before subtracting), so the stages form a hierarchical, collapse-resistant code. The output is the sum over stages; the returned single-integer code and perplexity are stage 0’s, keepingcell_codebook_idxa stable single column.num_embeddingsreports the per-stage code countprod(levels)andembedding_dimthe working dimension.- Parameters:
embedding_dim (
int) – Latent dimensionality (each stage projects to and fromlen(levels)).levels (
tuple[int,...], default:(8, 5, 5, 5)) – Per-dimension quantization levels shared by every stage (each >= 3).num_stages (
int, default:3) – Number of residual FSQ stages (>= 1).commitment_cost (
float, default:0.0) – Passed to each stage’sFSQ(FSQ is codebook-free; both default off).diversity_weight (
float, default:0.0) – Passed to each stage’sFSQ(FSQ is codebook-free; both default off).
- forward(inputs)#
Quantize the residual with a fresh FSQ at each stage and sum the quantized outputs.
- Parameters:
inputs (torch.Tensor) – Encoded latent, shape
(B, D, T)(channels-first; the model usesT=1), withD = embedding_dim.- Return type:
- Returns:
loss (torch.Tensor) – Scalar sum of the per-stage FSQ losses (
0unless a stage penalty is on).quantized (torch.Tensor) – Sum of the per-stage quantized vectors, shape
(B, D, T).perplexity (torch.Tensor) – Scalar perplexity of stage 0.
encoding_indices (torch.Tensor) – Stage-0 (coarse) mixed-radix code index per row, shape
(B*T, 1), in[0, prod(levels)).Shape
—–
- Input (
(B, D, T).)- Output (
loss(),quantized(B, D, T),perplexity) –(),encoding_indices(B*T, 1).
- class nicheverse.models.GroupedResidualVQ(num_embeddings, embedding_dim, commitment_cost=0.25, num_groups=2, num_stages=2, distance_metric='l2')#
Bases:
_VQDelegate,ModuleGrouped residual VQ: split the channels into groups, an
ResidualVQper group.The channel dimension is split into
num_groupsequal chunks; each chunk is quantized by its own residual VQ, and the quantized chunks are concatenated back to the full width (Yang et al., HiFi-Codec, arXiv:2305.02765). The loss is the sum over groups; the returned single-integer code and perplexity are group 0’s stage-0 values, keepingcell_codebook_idxa stable single column.embedding/num_embeddings/embedding_dim/distance_metricdelegate to group 0 (soembedding_dimreports the per-group width).- Parameters:
num_embeddings (
int) – Per-stage codebook size and total latent dimensionality (must be divisible bynum_groups).embedding_dim (
int) – Per-stage codebook size and total latent dimensionality (must be divisible bynum_groups).commitment_cost (
float, default:0.25) – Passed to each group’sResidualVQ.distance_metric (
str, default:'l2') – Passed to each group’sResidualVQ.num_groups (
int, default:2) – Number of channel groups the latent is split into.num_stages (
int, default:2) – Number of residual stages within each group’sResidualVQ.
- forward(inputs)#
Split into channel groups, run an independent residual VQ per group, and concatenate.
- Parameters:
inputs (torch.Tensor) – Encoded latent, shape
(B, D, T)(channels-first; the model usesT=1), withDdivisible bynum_groups.- Return type:
- Returns:
loss (torch.Tensor) – Scalar sum of the per-group residual VQ losses.
quantized (torch.Tensor) – Concatenation of the per-group quantized latents, shape
(B, D, T).perplexity (torch.Tensor) – Scalar perplexity of group 0’s stage-0 codebook.
encoding_indices (torch.Tensor) – Group-0 stage-0 code index per row, shape
(B*T, 1).Shape
—–
- Input (
(B, D, T).)- Output (
loss(),quantized(B, D, T),perplexity) –(),encoding_indices(B*T, 1).
- nicheverse.models.build_quantizer(name, *, num_embeddings, embedding_dim, commitment_cost, distance_metric='l2', **kwargs)#
Construct a quantizer by registry name (see
_QUANTIZERSfor the full set).build_quantizer("vq", ...)returns exactly theVectorQuantizerthe model builds by default, so the default path is bit-identical. The codebook-free variants (fsq,lfq,bsq,residual_fsq) derive their code count implicitly and ignorenum_embeddings(a warning is logged if the two disagree).
- nicheverse.models.register_quantizer(name)#
Class decorator registering a quantizer under
nameforbuild_quantizer().- Parameters:
name (
str)
Encoders#
Encoder backbones map each cell’s gene vector to a latent, chosen with
ModelConfig.encoder_type and configured with ModelConfig.encoder_kwargs.
All share the builder signature (in_dim, out_dim, hidden, dropout, **kwargs)
and map (batch, in_dim) to (batch, out_dim).
|
Backbone |
|
|---|---|---|
|
Deep, wide SwiGLU pre-norm residual MLP (no per-gene numerical embedding) |
|
|
Plain MLP: Linear, BatchNorm, ReLU, Dropout |
(none) |
|
MLP with per-gene periodic numerical embeddings and a SwiGLU trunk |
|
|
Pre-activation residual MLP blocks |
(none) |
|
Gene-patch Transformer |
|
|
1D convolutional network over the gene vector |
|
|
Lightweight 1D CNN |
|
|
Deep residual 1D CNN with squeeze-and-excitation and multiscale fusion |
|
|
Graph-attention message passing over gene-patch tokens |
|
|
U-Net denoiser encoder |
|
|
Diffusion Transformer with AdaLN conditioning |
|
|
Set Transformer (ISAB, PMA pooling) over gene-patch tokens |
|
|
Perceiver IO latent cross-attention |
|
|
Soft Mixture-of-Experts routing |
|
|
FT-Transformer with per-gene feature tokens (compute-prohibitive at cohort scale) |
|
- nicheverse.models.build_encoder(name, *, in_dim, out_dim, hidden, dropout=0.2, **kwargs)#
Build an encoder by registry name.
"mlp"(default) is the released encoder.Extra keyword arguments are forwarded to the encoder builder (e.g.
patch_size/num_heads/num_layersfor"transformer").
- nicheverse.models.register_encoder(name)#
Register an encoder builder (callable
(in_dim, out_dim, hidden, dropout) -> nn.Module).- Parameters:
name (
str)
- class nicheverse.models.ResidualMLP(in_dim, out_dim, hidden, dropout=0.2)#
Bases:
ModulePre-activation residual MLP encoder (LayerNorm, Linear, GELU, Dropout, Linear + skip).
- Parameters:
- class nicheverse.models.TransformerEncoder(in_dim, out_dim, hidden, dropout=0.2, patch_size=16, num_heads=4, num_layers=2)#
Bases:
ModuleGene-patch Transformer encoder: patchify the gene vector, self-attend, mean-pool to a latent.
Models combinatorial (nonlinear) gene interactions an MLP only mixes additively. Operates on the existing per-cell gene vector, no extra inputs.
- Parameters:
in_dim (
int) – Input gene count and output latent dimensionality.out_dim (
int) – Input gene count and output latent dimensionality.dropout (
float, default:0.2) – Attention and feed-forward dropout.patch_size (
int, default:16) – Genes per patch token.num_heads (
int, default:4) – Requested attention heads (reduced to the largest divisor of the width).num_layers (
int, default:2) – Number of Transformer encoder layers.
- forward(x)#
Patchify, self-attend over gene patches, mean-pool, and project to a latent.
Shape#
Input:
(B, in_dim)gene vector (right-padded to a multiple ofpatch_size).Intermediate:
(B, n_patches, hidden[0])patch tokens.Output:
(B, out_dim)latent embedding (mean over patch tokens).
- param x:
- type x:
Molecule-set model#
An alternative input modality: instead of a per-cell aggregated gene vector, each
cell is encoded as the set of its transcript molecules (one token per molecule,
carrying gene identity and subcellular offset) through a permutation-invariant,
mask-aware Set Transformer. :class:~nicheverse.models.MoleculeSetVQVAE mirrors
:class:~nicheverse.HierarchicalVQVAE (cell codebook, optional neighborhood
codebook, cross-attention fusion) and reuses the released
:class:~nicheverse.VectorQuantizer, so the codebook behavior and emitted code
indices are unchanged.
- class nicheverse.models.MoleculeSetEncoder(out_dim=64, width=128, n_genes=366, num_freqs=12, num_heads=4, num_inds=16, num_isab=2, dropout=0.1, max_micron=7.0)#
Bases:
ModuleSet Transformer++ over a cell’s molecule set -> per-cell latent.
Each molecule token is
gene_embedding(gene_id) + posMLP(Fourier(dx, dy)); masked ISAB blocks mix the set; the set is pooled by concatenating masked max, masked mean, and a masked PMA seed readout, then projected toout_dim. PMA alone collapses (its single seed query converges to a near-constant readout, so the codebook EMA chases it into one cluster); the max/mean statistics preserve each cell’s molecular-composition magnitude and spread, giving the quantizer real structure. For an all-padded (empty) cell the masked max and masked mean are already 0, but the PMA seed reads a garbage padded token, so it is zeroed for empty rows, giving a fully zero pooled embedding. Permutation invariant and mask aware (True= real molecule;gene_id == n_genesis the padding token).- Parameters:
- class nicheverse.models.MoleculeSetVQVAE(n_genes=366, cell_embedding_dim=64, cell_num_embeddings=256, neighborhood_embedding_dim=256, neighborhood_num_embeddings=32, commitment_cost=0.25, hidden=(256, 128), use_neighborhood=True, use_cross_attention=True, cross_attention_weight=0.5, enc_width=128, enc_isab=2, enc_heads=4, enc_inds=16, enc_freqs=12)#
Bases:
ModuleHierarchical VQ-VAE with a molecule-set cell branch and an optional aggregated kNN neighborhood branch, fused by cross-attention (mirrors
HierarchicalVQVAE). Emits the same cell/neighborhood code indices so downstream annotation is unchanged.- Parameters:
n_genes (
int, default:366)cell_embedding_dim (
int, default:64)cell_num_embeddings (
int, default:256)neighborhood_embedding_dim (
int, default:256)neighborhood_num_embeddings (
int, default:32)commitment_cost (
float, default:0.25)use_neighborhood (
bool, default:True)use_cross_attention (
bool, default:True)cross_attention_weight (
float, default:0.5)enc_width (
int, default:128)enc_isab (
int, default:2)enc_heads (
int, default:4)enc_inds (
int, default:16)enc_freqs (
int, default:12)
- forward(gene, coords, mask, neigh_features=None)#
Define the computation performed at every call.
Should be overridden by all subclasses.
Note
Although the recipe for forward pass needs to be defined within this function, one should call the
Moduleinstance afterwards instead of this since the former takes care of running the registered hooks while the latter silently ignores them.
Data#
Reading imaging spatial transcriptomics into an AnnData and building the
spatial neighbor graph the model consumes.
- class nicheverse.SpatialDataset(cell_features, spatial_coords, sample_ids, k_neighbors=20, neighborhood_aggregation='weighted_mean', spatial_graph='knn_radius', radius=50.0, bandwidth=None, agg_chunk=100000, recon_target=None)#
Bases:
DatasetCell + neighborhood-aggregated feature pairs, computed per sample.
- Parameters:
cell_features (
Any) –(n_cells, n_features)dense ndarray or scipy sparse matrix.spatial_coords (
Any) –(n_cells, 2)physical coordinates in microns.sample_ids (
Any) –(n_cells,)string label per cell. The spatial graph is computed within sample only; cells from different samples never become neighbors.k_neighbors (
int, default:20) – Neighbors per cell (excluding self) for the"knn"graph, and the cap on neighbors kept for"radius"/"delaunay". If a sample has fewer thank_neighbors + 1cells, all available cells are used. A single-cell sample falls back to duplicating the cell’s own vector.neighborhood_aggregation (
str, default:'weighted_mean') – One of{"mean", "weighted_mean", "max", "gaussian", "inverse_square"}."weighted_mean"uses1 / (distance + epsilon)weights,"inverse_square"uses1 / (distance ** 2 + epsilon), and"gaussian"usesexp(-distance ** 2 / (2 * bandwidth ** 2)).spatial_graph (
str, default:'knn_radius') – One of{"knn", "radius", "delaunay", "alpha_complex"}."knn"(default) keeps thek_neighborsnearest cells."radius"keeps every cell withinradiusmicrons (capped atk_neighbors)."delaunay"uses the Delaunay triangulation adjacency;"alpha_complex"additionally prunes long triangulation edges. Both fall back to a"knn"graph for samples with fewer than three cells or degenerate (collinear) coordinates.radius (
float|None, default:50.0) – Neighborhood radius in microns; required whenspatial_graph="radius".bandwidth (
Optional[float], default:None) – Gaussian kernel bandwidth in microns; required whenneighborhood_aggregation="gaussian".agg_chunk (
int, default:100000) – Cells per vectorized aggregation chunk. Bounds peak memory. Defaults to 100,000.recon_target (Any | None)
- Raises:
ValueError – If
neighborhood_aggregation/spatial_graphis unsupported, if input shapes are inconsistent, ifk_neighborsis non-positive, ifspatial_graph="radius"without a positiveradius, or ifneighborhood_aggregation="gaussian"without a positivebandwidth.
Notes
Determinism. The neighbor search produces a deterministic ordering given deterministic input: the index into
sample_idsdefines the row ordering inside each per-sample subset,np.uniquereturns samples in sorted order, and ties at the boundary distance are broken in input-row order.- Parameters:
- classmethod from_anndata(adata, sample_col='sample_id', spatial_key='spatial', x_col=None, y_col=None, coord_scale=1.0, transcript_context_key=None, recon_target_layer=None, **kwargs)#
Build a dataset from an AnnData of any imaging spatial-transcriptomics platform.
Coordinates are taken from
obsm[spatial_key]when present, otherwise from theobscolumnsx_col/y_col, and multiplied bycoord_scaleto convert to microns (e.g. CosMx pixels usecoord_scale=0.12028). Ifsample_colis absent, all cells are treated as a single sample.- Parameters:
adata (
Any) – AnnData with a cell-by-geneXand spatial coordinates.sample_col (
str, default:'sample_id') –obscolumn naming the per-sample (or per-FOV) unit.spatial_key (
str, default:'spatial') –obsmkey holding an(n_cells, 2)coordinate array.x_col (
Optional[str], default:None) – Fallbackobscoordinate columns used whenobsm[spatial_key]is absent.y_col (
Optional[str], default:None) – Fallbackobscoordinate columns used whenobsm[spatial_key]is absent.coord_scale (
float, default:1.0) – Multiplier converting the stored coordinates to microns.transcript_context_key (
Optional[str], default:None) – If given, concatenateobsm[transcript_context_key](segmentation-free transcript-context features fromtranscript_context()) ontoXto form the cell features. The modelinput_dimmust then beX.shape[1] + obsm[key].shape[1].recon_target_layer (
Optional[str], default:None) – If given, take the per-cell raw-count reconstruction target fromadata.layers[recon_target_layer](shape must matchX). Used by the count-recon cell modes (cell_reconin{"nb","poisson","both"}) so the encoder can see log1pXwhile the likelihood models the raw counts.None(default) leaves the dataset with no separate recon target (MSE reconstructs the input).**kwargs (
Any) – Forwarded toSpatialDataset(k_neighbors,spatial_graph, …).
- Return type:
The spatial graph mode is chosen with SpatialDataset.from_anndata(..., spatial_graph=...) (mirrored by TrainConfig.spatial_graph): knn_radius
(default, a k-NN graph whose edges are capped at radius microns, default
50), knn (pure k-NN, no cap), radius, delaunay, alpha_complex,
gabriel, and rng. The graph is always built per sample, so cells from
different samples never link. Passing transcript_context_key concatenates a
segmentation-free molecular field (see :func:~nicheverse.data.transcript_context)
onto the counts, doubling the model input dimension.
- nicheverse.read_spatial(source, sample_col='sample_id', spatial_key='spatial', x_col=None, y_col=None, coord_scale=1.0, copy=False)#
Standardize an AnnData from any imaging spatial-transcriptomics platform.
Accepts an AnnData or an
.h5adpath and returns an AnnData ready fortrain_model(),Trainer, orpredict_codes(). It ensuresobsm[spatial_key]holds micron coordinates (building it fromobs[x_col]/obs[y_col]and scaling bycoord_scalewhen needed) and thatobs[sample_col]exists.- Parameters:
source (
Any) – An AnnData or a path to an.h5adfile.sample_col (
str, default:'sample_id') –obscolumn naming the per-sample (or per-FOV) unit; created as a single sample if absent.spatial_key (
str, default:'spatial') –obsmkey for the(n_cells, 2)micron coordinates.x_col (
Optional[str], default:None) –obscoordinate columns used to buildobsm[spatial_key]when absent.y_col (
Optional[str], default:None) –obscoordinate columns used to buildobsm[spatial_key]when absent.coord_scale (
float, default:1.0) – Multiplier converting coordinates to microns (e.g. CosMx pixels use0.12028).copy (
bool, default:False) – IfTrueandsourceis already an AnnData, operate on a copy.
- Returns:
The standardized AnnData.
- Return type:
- nicheverse.read_xenium(run_dir, sample_id=None, drop_controls=True)#
Load one Xenium output directory into an
anndata.AnnData.- Parameters:
run_dir (
str|Path) – Path to a Xenium output directory containingcell_feature_matrix.h5andcells.parquet(or their fallbacks).sample_id (
Optional[str], default:None) – Label written toobs['sample_id']and appended to every barcode ascell_id__sample_id. Defaults toPath(run_dir).name.drop_controls (
bool, default:True) – Drop control / blank / unassigned / negative-control / codeword probes fromvar(case-insensitive match).
- Returns:
Cells x genes matrix with
obs['sample_id'],obsm['spatial'](microns), andobsjoined with the contents ofcells.parquet.- Return type:
AnnData
- Raises:
FileNotFoundError – If
run_dirdoes not exist or is missing required files.ValueError – If
cells.parquetis missing the requiredx_centroid/y_centroidcolumns.
- nicheverse.read_xenium_cohort(run_dirs, sample_ids=None, drop_controls=True, manifest=None)#
Merge multiple Xenium runs into a single
anndata.AnnData.- Parameters:
run_dirs (
Iterable[str|Path] |str|Path) – One Xenium output directory, an iterable of such directories, or unused (pass[]) whenmanifestis provided.sample_ids (
Optional[Iterable[str]], default:None) – Optional iterable of sample IDs matchingrun_dirslength. Defaults toPath(p).namefor each run.drop_controls (
bool, default:True) – Drop control / blank / unassigned / codeword probes (seeload_xenium_run()).manifest (
Union[str,Path,None], default:None) – CSV with columnsrun_dirandsample_id. If provided, takes precedence overrun_dirs/sample_ids.
- Returns:
Cohort AnnData restricted to genes shared across all runs, with a categorical
obs['sample_id'].- Return type:
AnnData
- Raises:
ValueError – If
run_dirsis empty and no manifest is given, lengths mismatch, or no genes are shared across the runs.
- nicheverse.load_xenium_run(run_dir, sample_id=None, drop_controls=True)#
Load one Xenium output directory into an
anndata.AnnData.- Parameters:
run_dir (
str|Path) – Path to a Xenium output directory containingcell_feature_matrix.h5andcells.parquet(or their fallbacks).sample_id (
Optional[str], default:None) – Label written toobs['sample_id']and appended to every barcode ascell_id__sample_id. Defaults toPath(run_dir).name.drop_controls (
bool, default:True) – Drop control / blank / unassigned / negative-control / codeword probes fromvar(case-insensitive match).
- Returns:
Cells x genes matrix with
obs['sample_id'],obsm['spatial'](microns), andobsjoined with the contents ofcells.parquet.- Return type:
AnnData
- Raises:
FileNotFoundError – If
run_dirdoes not exist or is missing required files.ValueError – If
cells.parquetis missing the requiredx_centroid/y_centroidcolumns.
- nicheverse.load_xenium_cohort(run_dirs, sample_ids=None, drop_controls=True, manifest=None)#
Merge multiple Xenium runs into a single
anndata.AnnData.- Parameters:
run_dirs (
Iterable[str|Path] |str|Path) – One Xenium output directory, an iterable of such directories, or unused (pass[]) whenmanifestis provided.sample_ids (
Optional[Iterable[str]], default:None) – Optional iterable of sample IDs matchingrun_dirslength. Defaults toPath(p).namefor each run.drop_controls (
bool, default:True) – Drop control / blank / unassigned / codeword probes (seeload_xenium_run()).manifest (
Union[str,Path,None], default:None) – CSV with columnsrun_dirandsample_id. If provided, takes precedence overrun_dirs/sample_ids.
- Returns:
Cohort AnnData restricted to genes shared across all runs, with a categorical
obs['sample_id'].- Return type:
AnnData
- Raises:
ValueError – If
run_dirsis empty and no manifest is given, lengths mismatch, or no genes are shared across the runs.
- nicheverse.spatial_neighbors(adata, k_neighbors=20, neighborhood_aggregation='weighted_mean', spatial_graph='knn_radius', radius=50.0, bandwidth=None, sample_col='sample_id', key_added='neighborhood_features', copy=False)#
Compute per-sample aggregated neighborhood features and store them in
obsm.The neighborhood feature for each cell is the concatenation of its own expression and an aggregation over its spatial neighbors, computed within each sample so cross-sample edges never form (see
SpatialDataset).- Parameters:
adata (
AnnData) – AnnData withobsm['spatial'](microns) andobs[sample_col].k_neighbors (
int, default:20) – Forwarded toSpatialDataset.neighborhood_aggregation (
str, default:'weighted_mean') – Forwarded toSpatialDataset.spatial_graph (
str, default:'knn_radius') – Forwarded toSpatialDataset.radius (
float|None, default:50.0) – Forwarded toSpatialDataset.bandwidth (
Optional[float], default:None) – Forwarded toSpatialDataset.sample_col (
str, default:'sample_id') –obscolumn partitioning cells for the per-sample graph.key_added (
str, default:'neighborhood_features') –obsmkey to write the(n_cells, 2 * n_genes)feature matrix to.copy (
bool, default:False) – IfTrue, operate on and return a copy; otherwise write in place and return the(n_cells, 2 * n_genes)ndarray.
- Returns:
The mutated (or copied) AnnData when
copy=True, else the feature matrix ndarray.- Return type:
AnnData or numpy.ndarray
- nicheverse.data.transcript_context(adata, transcripts, radius=7.0, sample_col='sample_id', platform='xenium', x_col=None, y_col=None, feature_col=None, control_pattern=None, key_added='transcript_context', copy=False)#
Compute the per-cell local molecular field and store it in
obsm.- Parameters:
adata (
AnnData) – AnnData withobsm['spatial'](microns) andobs[sample_col].transcripts (
dict|str|Path) – Either a mapping{sample_id: parquet_path}(one molecule table per sample) or a single path used for every sample (single-sample runs).radius (
float, default:7.0) – Micron radius of the molecular field around each centroid.platform (
str, default:'xenium') – Molecule-table convention:"xenium"(x_location/y_location/feature_name),"cosmx"(x_global_px/y_global_px/target), or"merfish"(global_x/global_y/gene). Sets column and control-probe defaults.x_col (
Optional[str], default:None) – Molecule table column names; override theplatformdefaults when given.y_col (
Optional[str], default:None) – Molecule table column names; override theplatformdefaults when given.feature_col (
Optional[str], default:None) – Molecule table column names; override theplatformdefaults when given.control_pattern (
Optional[Pattern], default:None) – Compiled regex of control/blank probe names to drop; defaults to theplatformpattern.key_added (
str, default:'transcript_context') –obsmkey for the(n_cells, n_genes)log1p count matrix.copy (
bool, default:False) – If True, operate on and return a copy; else write in place and return the feature matrix.sample_col (
str, default:'sample_id')
- Raises:
ValueError – If
obsm['spatial']/obs[sample_col]is missing, or a sample has no transcripts path.- Return type:
AnnData|ndarray
- nicheverse.attach_codes(adata, cell_idx, neigh_idx, cell_emb=None, neigh_emb=None)#
Write codebook indices and (optionally) embeddings back into AnnData.
Indices are stored as the narrowest integer dtype that fits their range, saving disk space for the typical case where the codebook has at most a few hundred entries.
- Parameters:
adata (
AnnData) – AnnData withadata.n_obscells; mutated in place.cell_idx (
ndarray) –(n_obs,)integer arrays of code assignments.neigh_idx (
ndarray) –(n_obs,)integer arrays of code assignments.cell_emb (
Optional[ndarray], default:None) – Optional(n_obs, D)continuous embeddings written toadata.obsm.neigh_emb (
Optional[ndarray], default:None) – Optional(n_obs, D)continuous embeddings written toadata.obsm.
- Returns:
The same
adatawith new columns and (optionally) obsm matrices.- Return type:
AnnData
- Raises:
ValueError – If any input array length differs from
adata.n_obs.
- nicheverse.attach_codes_to_adata(adata, cell_idx, neigh_idx, cell_emb=None, neigh_emb=None)#
Write codebook indices and (optionally) embeddings back into AnnData.
Indices are stored as the narrowest integer dtype that fits their range, saving disk space for the typical case where the codebook has at most a few hundred entries.
- Parameters:
adata (
AnnData) – AnnData withadata.n_obscells; mutated in place.cell_idx (
ndarray) –(n_obs,)integer arrays of code assignments.neigh_idx (
ndarray) –(n_obs,)integer arrays of code assignments.cell_emb (
Optional[ndarray], default:None) – Optional(n_obs, D)continuous embeddings written toadata.obsm.neigh_emb (
Optional[ndarray], default:None) – Optional(n_obs, D)continuous embeddings written toadata.obsm.
- Returns:
The same
adatawith new columns and (optionally) obsm matrices.- Return type:
AnnData
- Raises:
ValueError – If any input array length differs from
adata.n_obs.
The subcellular molecule-set representation (a per-cell transcript point cloud) is
loaded by :class:~nicheverse.data.MoleculeSetDataset and consumed by
:class:~nicheverse.models.MoleculeSetVQVAE.
- class nicheverse.data.MoleculeSetDataset(obs_names, shard_dir, spatial_coords=None, sample_ids=None, expr_lognorm=None, k_neighbors=20, neighborhood_aggregation='weighted_mean', with_neighborhood=True)#
Bases:
DatasetPer-cell molecule sets aligned to
obs_names(+ optional kNN neighborhood).- Parameters:
obs_names – Cell identifiers defining the row order (aligns shards to the model adata).
shard_dir – Directory of
*.npzmolecule-set shards.spatial_coords (default:
None) – Passed toSpatialDatasetto build the neighborhood branch whenwith_neighborhoodis True.sample_ids (default:
None) – Passed toSpatialDatasetto build the neighborhood branch whenwith_neighborhoodis True.expr_lognorm (default:
None) – Passed toSpatialDatasetto build the neighborhood branch whenwith_neighborhoodis True.k_neighbors (
int, default:20) – Forwarded toSpatialDataset.neighborhood_aggregation (
str, default:'weighted_mean') – Forwarded toSpatialDataset.with_neighborhood (
bool, default:True) – If True, attach(n, 2*n_genes)aggregated neighborhood features.
Training#
- class nicheverse.Trainer(config=None, model_config=None)#
Bases:
objectHigh-level training and inference wrapper (nanoGPT / Lightning style).
- Parameters:
config (default:
None) – ATrainConfig; defaults to the reproducible production settings.model_config (default:
None) – OptionalModelConfig; if omitted,fitbuilds one from the AnnData gene panel.
Examples
>>> trainer = Trainer(TrainConfig(num_epochs=10)) >>> model, adata = trainer.fit(adata, "checkpoints/") >>> annotated = trainer.predict(new_adata, "checkpoints/hierarchical_vqvae_checkpoint.pt")
- fit(adata, checkpoint_dir, model_config=None, sample_col='sample_id', device=None)#
Train and persist the model; returns
(model, annotated_adata).
- predict(adata, checkpoint=None, **kwargs)#
Assign codes to
adatausing a checkpoint path or the fitted model.
- class nicheverse.TrainConfig(num_epochs=300, batch_size=32768, scale_lr_with_batch=True, learning_rate=0.0003, weight_decay=0.01, lr_schedule='plateau', warmup_steps=0, warmup_frac=0.03, min_lr=0.0, decoupled_weight_decay=True, spatial_loss_type='none', spatial_loss_weight=0.0, spatial_loss_k=6, cell_weight=1.0, neighborhood_weight=1.0, k_neighbors=20, neighborhood_aggregation='weighted_mean', spatial_graph='knn_radius', radius=50.0, bandwidth=None, normalize=True, log1p=True, val_fraction=0.0, early_stopping_patience=None, grad_clip=None, amp=False, resume_from=None, save_best=True, seed=9, log_every=10, deterministic=True, num_workers=0, ddp=False, compile_model=False, pin_memory=None, persistent_workers=None, prefetch_factor=None, device_resident=True, device_resident_mem_fraction=0.9)#
Bases:
objectHyperparameters for
train_model().- Parameters:
num_epochs (
int, default:300) – Number of full passes over the dataset.batch_size (
int|str, default:32768) – Cells per mini-batch. Anint(default32768) or the string"auto"to resolve it at train time from the panel size viaauto_batch_size(). Only"auto"triggers the adaptive path; any int is used verbatim. The default was raised from 2048 to 32768 following the large-batch literature (McCandlish 2018 critical batch size; Goyal 2017 linear-scaling + warmup; Smith 2018): at cohort scale (millions of cells) a much larger batch improves gradient-estimate quality and hardware utilization with no loss of final quality when paired with LR warmup (seelr_schedule='warmup_cosine'andwarmup_frac). A batch of 32768 was measured at ~45 GB resident on an 80 GB A100, so it fits thedevice_residentpath with headroom; the power-of-two floor logic inauto_batch_size()and thedevice_residentfit-check both hold for 32768. The default 32768 is the released b32k trajectory; pass a smaller int (e.g. 2048) only for small-GPU or quick runs.scale_lr_with_batch (
bool, default:True) – Whenbatch_size="auto"and this isTrue(default), scale the learning rate bysqrt(effective_batch / 2048)so a larger resolved batch keeps a comparable update magnitude. No effect for an int batch.learning_rate (
float, default:0.0003) – Adam learning rate. Reduced on plateau byfactor=0.5withpatience=5epochs (monitors the validation loss whenval_fraction > 0, else the training loss).weight_decay (
float, default:0.01) – AdamW decoupled weight decay coefficient (Loshchilov and Hutter 2019).0.01(default) is the AdamW reference value and is conservative for a VQ tokenizer trained on millions of cells (large data, low overfitting risk). Applied ONLY whendecoupled_weight_decayisTrue(the default), and then only to the 2-D-or-wider weight matrices ofnn.Linear/nn.Conv*and the attention QKV projection; every other parameter (both VQ codebook embeddings, the molecule-set gene embedding, allnn.LayerNorm/nn.BatchNormscales, all biases, and every barenn.Parametersuch as themlp_plrperiodic-embedding tensors) is excluded. Set to0to recover plain AdamW with no regularization.cell_weight (
float, default:1.0) – Multipliers on the two reconstruction + commitment loss branches.neighborhood_weight (
float, default:1.0) – Multipliers on the two reconstruction + commitment loss branches.k_neighbors (
int, default:20) – Neighbors used to build the neighborhood feature.neighborhood_aggregation (
str, default:'weighted_mean') – One of{"mean", "weighted_mean", "max", "gaussian", "inverse_square"}.spatial_graph (
str, default:'knn_radius') – Spatial graph backend:{"knn", "knn_radius" (default), "radius", "delaunay", "alpha_complex", "gabriel", "rng"}.radius (
float|None, default:50.0) – Radius in microns for the radius-capped graphs (spatial_graph="knn_radius"or"radius"). Default50.0.bandwidth (
Optional[float], default:None) – Gaussian kernel bandwidth in microns forneighborhood_aggregation="gaussian".lr_schedule (
str, default:'plateau') –"plateau"(default, released) or"warmup_cosine". The"warmup_cosine"schedule is applied PER OPTIMIZER STEP overtotal_steps = steps_per_epoch * num_epochs: a linear warmup over the firstwarmup_fracof the steps, then a cosine decay tomin_lrat the final step (Goyal 2017; Nicheformer 2025 uses warmup + cosine at spatial scale).warmup_frac (
float, default:0.03) – Fraction of total optimizer steps spent linearly ramping the LR from ~0 up to the peak (base) LR before the cosine decay begins, for"warmup_cosine".0.03(default).0.0reproduces a pure per-step cosine anneal with no warmup (back-compatible). Ignored by the plateau schedule.warmup_steps (
int, default:0) –min_lris the learning-rate floor the cosine decays to at the final step for"warmup_cosine"(0.0decays fully to zero).warmup_stepsis a legacy epoch-based field retained for API compatibility; the per-step"warmup_cosine"schedule useswarmup_fracfor its warmup length.min_lr (
float, default:0.0) –min_lris the learning-rate floor the cosine decays to at the final step for"warmup_cosine"(0.0decays fully to zero).warmup_stepsis a legacy epoch-based field retained for API compatibility; the per-step"warmup_cosine"schedule useswarmup_fracfor its warmup length.decoupled_weight_decay (
bool, default:True) – Use selective (decoupled) AdamW weight decay: decay ONLY the weight matrices ofnn.Linear/nn.Conv*(and the attention QKV projection), and exclude everything else (both VQ codebook embeddings, the molecule-set gene embedding, all norm scales, all biases, and all barenn.Parametertensors including themlp_plrfreq/emb_w/emb_bperiodic-embedding parameters).True(default). WhenFalse, a single AdamW group appliesweight_decayuniformly to every parameter (including the codebooks), which is discouraged for a VQ tokenizer because decaying codebook vectors toward zero shrinks code norms and distorts nearest-code assignment geometry.spatial_loss_type (
str, default:'none') – Optional spatial-coherence regularizer on the cell embedding ("laplacian"/"contrastive"/"codebook_consistency"), its weight (0disables, the default), and its neighbor count. The graph is built within each mini-batch and restricted to same-sample pairs, so it is most effective with a largebatch_size.spatial_loss_weight (
float, default:0.0) – Optional spatial-coherence regularizer on the cell embedding ("laplacian"/"contrastive"/"codebook_consistency"), its weight (0disables, the default), and its neighbor count. The graph is built within each mini-batch and restricted to same-sample pairs, so it is most effective with a largebatch_size.spatial_loss_k (
int, default:6) – Optional spatial-coherence regularizer on the cell embedding ("laplacian"/"contrastive"/"codebook_consistency"), its weight (0disables, the default), and its neighbor count. The graph is built within each mini-batch and restricted to same-sample pairs, so it is most effective with a largebatch_size.normalize (
bool, default:True) – Applysc.pp.normalize_total/sc.pp.log1ptoadata.Xbefore training. Skipped if the AnnData is already log-normalized.log1p (
bool, default:True) – Applysc.pp.normalize_total/sc.pp.log1ptoadata.Xbefore training. Skipped if the AnnData is already log-normalized.val_fraction (
float, default:0.0) – Fraction of cells held out for a validation loss.0(default) trains on all cells and monitors the training loss (released behavior).early_stopping_patience (
Optional[int], default:None) – Stop if the monitored loss does not improve for this many epochs.None(default) disables early stopping.grad_clip (
Optional[float], default:None) – Max global gradient norm (torch.nn.utils.clip_grad_norm_).None(default) disables clipping.amp (
bool, default:False) – Enable CUDA automatic mixed precision.False(default) keeps full fp32 for bit-stable determinism.resume_from (
Optional[str], default:None) – Optional checkpoint.ptpath to resume model weights from.save_best (
bool, default:True) – IfTrueand a validation split is used, also writebest_checkpoint.ptat the epoch with the lowest monitored loss.seed (
int, default:9) – Seed passed tonicheverse.utils.seed_everything().log_every (
int, default:10) – Emit a batch-level log line everylog_everymini-batches.deterministic (
bool, default:True) – Forwarded toseed_everything().num_workers (
int, default:0) – DataLoader workers.ddp (
bool, default:False) – Opt-in multi-GPU DistributedDataParallel. WhenTrueand the process was launched undertorchrun(WORLD_SIZE > 1), the model is wrapped in DDP and the global batch is split across ranks (per-GPU batch =batch_size // world_size), so optimization matches a single-GPU run on the full global batch within numerical tolerance. The EMA VQ codebook statistics are all-reduced so the codebook is rank-correct. DefaultFalseand a no-op whenWORLD_SIZE == 1(single-GPU byte-identical). SeeDETERMINISM.mdfor the one documented residual (dead-code reset RNG).compile_model (
bool, default:False) – Opt-intorch.compile(model)for a faster (accuracy-preserving) forward / backward. DefaultFalse. The compiled graph is numerically equivalent to eager for these ops; only kernel fusion changes.pin_memory (
Optional[bool], default:None) – Explicit DataLoaderpin_memory.None(default) auto-selectsTrueon CUDA, preserving the released behavior. Verified accuracy neutral (byte-identical loss withnum_workers=0).persistent_workers (
Optional[bool], default:None) – Explicit DataLoaderpersistent_workers.None(default) isFalseso the epoch-by-epoch shuffle order matchesnum_workers=0exactly; setTrueto save worker respawn overhead at the cost of a different (still valid) shuffle for epochs after the first.prefetch_factor (
Optional[int], default:None) – DataLoaderprefetch_factor(only applies whennum_workers > 0).None(default) uses the PyTorch default (2). Verified accuracy neutral.device_resident (
bool, default:True) –GPU-resident dataset that eliminates the per-batch host->device copy. Now default ON (automatic): the trainer uses the GPU-resident path whenever a CUDA GPU is present, DDP is off, and the dataset fits the GPU’s free-memory budget (see
_device_resident_fits()); otherwise it transparently falls back to the CPU DataLoader. SetFalseto force the released CPU-DataLoader path. When active, the feature tensors (cell_features,neighborhood_featuresand, in the count modes,recon_target/niche_count_target) are moved to the GPU ONCE after dataset construction; the DataLoader then yields only the seeded batch INDEX tensor and the features are gathered on the GPU inside the loop (cb = cell_features_gpu[idx]). This is accuracy neutral: the SAME seeded shuffle/BatchSampler produces the SAME batch indices in the SAME order as the CPU path, so for a fixed seed the per-batch loss is equivalent to floating-point tolerance. Requires CUDA; on CPU it is a no-op (falls back to the CPU path). The fit-check is additive: the resident tensors plus a fixed ~8 GiB working-set margin must fit underdevice_resident_mem_fraction(default0.9) of the memory budget, where the budget is the FREE GPU memory (torch.cuda.mem_get_info) when queryable, else the device total. If they do not fit (wide panels, e.g. CosMx 21731 genes or a big merged Xenium), a clear message is logged and the normal CPU DataLoader path is used instead (never OOMs silently). Because the resident tensors cannot be forked to DataLoader workers,num_workersis forced to0when this path is active (logged). Not currently combined with DDP: under DDP this flag is ignored (logged) and the CPU path is used.Caveat: strictly byte-identical reproduction of a released checkpoint should pass
device_resident=False. The on-GPU gather shifts floating-point accumulation by ~1e-5 (accuracy neutral, but not bit-identical to the CPU-DataLoader run that produced the release).device_resident_mem_fraction (
float, default:0.9) – Fraction of the GPU memory budget the resident tensors + working margin may occupy in thedevice_residentfit-check.0.9(default). Lower it to be more conservative on a shared card.
Notes
num_workers=0(the default) is byte-identical to the released training run.pin_memoryandprefetch_factorare verified to leave the loss trajectory byte-identical.num_workers > 0is a throughput knob for I/O-bound cohorts (large on-disk data); on small in-memory cohorts it can shift the trajectory to a different (equally valid) one, so it is not guaranteed bit-identical tonum_workers=0. Prefernum_workers=0when exact reproduction of the released run is required.
- nicheverse.train_model(adata, checkpoint_dir, model_config=None, train_config=None, sample_col='sample_id', device=None)#
Train
HierarchicalVQVAEonadataand persist checkpoint + outputs.- Parameters:
adata (
AnnData) – Input AnnData. Required:adata.obsm['spatial'](microns) andadata.obs[sample_col].adata.Xis expected to be raw counts; per-cell normalization and log1p are applied pertrain_config.adatais copied before mutation; the original is left untouched.checkpoint_dir (
str|Path) – Output directory for the checkpoint, embeddings, loss curve, environment snapshot, and annotated AnnData. Created if missing.model_config (
Optional[ModelConfig], default:None) – OptionalModelConfig. Defaults to a config withinput_dim = adata.X.shape[1]andgene_names = adata.var_names.train_config (
Optional[TrainConfig], default:None) – OptionalTrainConfig. Defaults to the constructor defaults.sample_col (
str, default:'sample_id') – Column inadata.obsused to partition cells for the per-sample graph.device (
Optional[str], default:None) – Optional explicit device string (e.g."cuda:0"). Defaults to the first CUDA device if available, else CPU.
- Returns:
The trained model and the annotated AnnData copy with code indices and embeddings attached.
- Return type:
(HierarchicalVQVAE, AnnData)
- Raises:
ValueError – If
adata.obsm['spatial']oradata.obs[sample_col]is missing, or ifmodel_config.input_dimdisagrees withadata.X.shape[1].
- nicheverse.mae_pretrain(adata, encoder_type='mlp', hidden=(256, 128), embedding_dim=64, mask_ratio=0.5, num_epochs=20, batch_size=2048, lr=0.001, normalize=True, log1p=True, seed=9, device=None)#
Pretrain an encoder by masked-gene reconstruction; return the pretrained encoder.
A fraction
mask_ratioof genes is zeroed per cell; the encoder plus a light decoder reconstruct the full profile with the loss taken on masked positions only, biasing the encoder toward coexpression structure. The returned encoder has the same architecture as a model built with the matchingencoder_type/hidden/embedding_dimand can initialize itscell_encoderviaload_state_dict.
Prediction#
- nicheverse.predict_codes(adata, checkpoint, output_path=None, sample_col='sample_id', k_neighbors=None, neighborhood_aggregation=None, spatial_graph=None, radius=<object object>, bandwidth=None, batch_size=2048, normalize=True, log1p=True, device=None, return_embeddings=True, config=None, seed=9, deterministic=True)#
Assign cell and neighborhood codebook indices to
adatausing a checkpoint.- Parameters:
adata (
AnnData) – Input AnnData with raw counts (or already-normalized data, in which case setnormalize=Falseandlog1p=False). Must carryadata.obsm['spatial']andadata.obs[sample_col].checkpoint (
str|Path|HierarchicalVQVAE) – Either a path to a.ptfile written bynicheverse.train_model()or an already-loadedHierarchicalVQVAE.output_path (
Union[str,Path,None], default:None) – Optional.h5adpath to write the annotated AnnData. Parent directories are created if missing.sample_col (
str, default:'sample_id') – Column inadata.obsused to partition cells for per-sample k-NN.k_neighbors (
Optional[int], default:None) – Neighbors per cell.None(default) inherits the training-time value from the checkpoint’strain_config.jsonso the neighborhood codebook behaves as intended; pass an int to override.neighborhood_aggregation (
Optional[str], default:None) – One of{"mean", "weighted_mean", "max", "gaussian", "inverse_square"}.None(default) inherits the training-time value from the checkpoint.spatial_graph (
Optional[str], default:None) – Spatial-graph backend.None(default) inherits the training-time value from the checkpoint (e.g."knn_radius"); pass a string to override. Building the neighborhood graph with a different backend than training silently produces mismatched niche codes, so inheriting is the safe default.radius (
float|None, default:<object object at 0x7fa948ac2d70>) – Radius in microns for radius-based graphs. Left un-passed it inherits the training-time value from the checkpoint; pass an explicit value (includingNonefor graphs that take no radius) to override.bandwidth (
Optional[float], default:None) – Gaussian kernel bandwidth in microns; must match training whenneighborhood_aggregation="gaussian".Noneinherits the training-time value from the checkpoint.batch_size (
int, default:2048) – Inference mini-batch size.normalize (
bool, default:True) – Applysc.pp.normalize_total/sc.pp.log1pbefore inference. Set both to False if your AnnData is already log-normalized.log1p (
bool, default:True) – Applysc.pp.normalize_total/sc.pp.log1pbefore inference. Set both to False if your AnnData is already log-normalized.device (
Optional[str], default:None) – Optional explicit device string. Defaults to CUDA if available.return_embeddings (
bool, default:True) – If True, writeX_cell_embeddingandX_neighborhood_embeddingtoadata.obsm.config (
Optional[ModelConfig], default:None) – OptionalModelConfigfor legacy bare state dict checkpoints.seed (
int, default:9) – Seed for the random ops withinseed_everything().deterministic (
bool, default:True) – Forwarded toseed_everything().
- Returns:
Annotated copy of the input with
obs['cell_codebook_idx']andobs['neighborhood_codebook_idx'](plus embeddings inobsmifreturn_embeddings).- Return type:
AnnData
- Raises:
ValueError – If required obs / obsm fields are missing, or if the gene panel does not match the checkpoint.
Losses#
Optional spatial-coherence and codebook regularizers added to the reconstruction objective.
Reconstruction likelihoods and optional spatial-coherence losses.
This module holds two families of loss functions used by
nicheverse.train_model():
Count reconstruction likelihoods,
nb_nll()andpoisson_nll(), selected byModelConfig.reconwhen the decoder models raw counts instead of Gaussian (MSE) residuals.Spatial-coherence regularizers,
laplacian_smoothness(),spatial_contrastive()andcodebook_consistency(), collected in theSPATIAL_LOSSESregistry. Each operates on a batch of latent vectorszof shape(B, D)and the matching cell coordinatescoordsof shape(B, 2)(microns), building an intra-batch k-nearest-neighbor graph on the fly. They are opt-in: training applies one only whenTrainConfig.spatial_loss_weight > 0(default0), so the released training path is unaffected.
The spatial losses encourage the cell encoder to produce representations that vary smoothly across physical space, biasing neighboring cells toward the same (or nearby) discrete cell-state codes without hard-coding spatial information into the codebook itself.
Symbols#
B batch size, D latent dimensionality, k neighbors per cell.
- nicheverse.losses.NICHE_SPATIAL_LOSSES = frozenset({'graph_tv'})#
Spatial-loss names applied to the NICHE (neighborhood) latent instead of the cell latent.
"graph_tv"(L1 fused-lasso total variation) smooths niche assignments across the spatial graph, so the trainer feeds it the neighborhood encoder output; all other entries operate on the cell latent.
- nicheverse.losses.gaussian_nll(target, pred)#
Gaussian reconstruction loss (mean squared error), the default cell/niche head.
Up to an additive constant and a factor, the negative log-likelihood of a Gaussian with fixed unit variance is the mean squared error between the prediction and the target, so this is the MSE reconstruction term named as a likelihood for uniform, by-name dispatch alongside
nb_nll(),poisson_nll(), anddirichlet_multinomial_nll(). It is the released default for both the cell branch (on the log1p expression) and the niche branch (on the aggregated composition), and is numerically identical totorch.nn.functional.mse_loss(pred, target)(mean reduction).- Parameters:
target (torch.Tensor) – Reconstruction target (any shape).
pred (torch.Tensor) – Predicted reconstruction, same shape as
target.
- Returns:
Scalar mean squared error.
- Return type:
- nicheverse.losses.nb_nll(x, cr, log_theta, eps=1e-08, library=None)#
Negative-binomial negative log-likelihood (scVI mean-dispersion parameterization).
Overdispersed count reconstruction loss selected by
ModelConfig.recon="nb". The decoder emits per-gene logitscrthat are softmaxed into a proportion over the panel and scaled by the per-cell library size to give the NB meanmu;exp(log_theta)is the per-gene inverse-dispersion. This penalizes the model for assigning low probability to the observed integer counts and, unlike MSE, respects the mean-variance relationship of molecular counts, which better matches sparse Xenium panels. Used in place of the Gaussian (MSE) cell reconstruction term when raw counts are modeled directly.- Parameters:
x (torch.Tensor) – Observed raw counts, shape
(B, N)(N genes). Not normalized.cr (torch.Tensor) – Per-gene decoder logits, shape
(B, N); softmaxed to proportions and scaled bylibraryto form the NB meanmu.log_theta (torch.Tensor) – Per-gene log inverse-dispersion, shape
(N,)(a learnedmodel.cell_log_thetaparameter).eps (float, default=1e-8) – Numerical floor added inside logs.
library (torch.Tensor, optional) – Per-cell multiplier for the softmax proportion (the library / GLM offset), shape
(B,)or(B, 1). WhenNone(default, byte-identical to the released path) the observed count sumx.sum(1)is used, which is the standard scVI library. Pass an explicit per-cell size factor to override it (the trainer passes the observed total count of the raw-count target so the softmax proportion is scaled to the correct count scale even though the encoder input was the log1p expression).
- Returns:
Scalar mean NB negative log-likelihood over the batch.
- Return type:
- nicheverse.losses.poisson_nll(x, cr, eps=1e-08, library=None)#
Poisson negative log-likelihood with a library-size-scaled softmax mean.
Count reconstruction loss selected by
ModelConfig.recon="poisson". Likenb_nll()the decoder logitscrare turned into a proportion and scaled by the per-cell library size to form the Poisson ratemu, but with no separate dispersion parameter (variance equals mean). It is the simpler, equidispersed alternative for count reconstruction; prefernb_nll()when the data are overdispersed.- Parameters:
x (torch.Tensor) – Observed raw counts, shape
(B, N).cr (torch.Tensor) – Per-gene decoder logits, shape
(B, N); softmaxed and scaled by the library size to form the ratemu.eps (float, default=1e-8) – Numerical floor inside the log.
library (torch.Tensor, optional) – Per-cell multiplier for the softmax proportion (the library / GLM offset), shape
(B,)or(B, 1). WhenNone(default) the observed count sumx.sum(1)is used; pass an explicit per-cell size factor to override it. Seenb_nll().
- Returns:
Scalar mean Poisson negative log-likelihood over the batch.
- Return type:
- nicheverse.losses.dirichlet_multinomial_nll(target, logits, log_alpha, eps=1e-08)#
Dirichlet-multinomial negative log-likelihood for a compositional target.
A proper likelihood for a niche composition vector: the decoder
logitsare softmaxed into a mean compositionpand scaled by a learned per-feature concentrationexp(log_alpha)to form the Dirichlet parametersalpha = p * sum(exp(log_alpha))(a mean/precision parameterization). Thetargetis treated as a (possibly fractional) count vectorcwith totalN = c.sum(1), and the Dirichlet-multinomial log-density is evaluated withlgamma(well-defined for non-integerc, so it handles the continuous aggregated-neighbor composition without rounding). Unlike an MSE on the composition, it respects that the target is a normalized, overdispersed proportion. Opt-in viaModelConfig.niche_recon="dirichlet_multinomial".- Parameters:
target (torch.Tensor) – COUNT-SCALE compositional target, shape
(B, N), non-negative. Its per-row sum is used as the multinomial totalN, so this must be on the raw-count scale (e.g. the weighted-mean aggregation of the raw neighbor counts), NOT a log1p mean whose row sum has no count interpretation. Fractional values are allowed (the density is evaluated withlgamma), so a continuous count-scale aggregate is fine without rounding.logits (torch.Tensor) – Per-feature decoder logits, shape
(B, N); softmaxed to the mean composition.log_alpha (torch.Tensor) – Per-feature log-concentration, shape
(N,)(a learnedmodel.niche_log_alphaparameter).sum(exp(log_alpha))sets the overall precision.eps (float, default=1e-8) – Numerical floor.
- Returns:
Scalar mean Dirichlet-multinomial NLL over the batch.
- Return type:
- nicheverse.losses.bernoulli_detection_bce(x, logits)#
Binary-cross-entropy detection hurdle on the 0-vs-nonzero mask of counts.
A per-gene Bernoulli likelihood over whether each gene is DETECTED (count > 0) in a cell, using
logitsas the detection logits. Summing this with a count NLL forms a hurdle model that separates the probability of detection from the conditional count magnitude, which better fits the excess zeros of sparse imaging panels (Xenium). Opt-in viaModelConfig.detection_weight > 0.- Parameters:
x (torch.Tensor) – Observed raw counts, shape
(B, N). The target mask is(x > 0).logits (torch.Tensor) – Per-gene detection logits, shape
(B, N)(the cell decoder output is reused as the logits, so no extra head is needed).
- Returns:
Scalar mean BCE (summed over genes, averaged over cells).
- Return type:
- nicheverse.losses.graph_total_variation(z, coords, k=6)#
Graph total-variation (L1 fused-lasso) penalty on latents across neighbors.
The L1 analogue of
laplacian_smoothness(): it penalizes the mean L1 (not squared-L2) difference between each cell’s latent and itskspatial nearest neighbors,mean_i mean_j |z_i - z_j|_1. The L1 form is the fused lasso / anisotropic total variation, which promotes piecewise-constant latent fields (sharp niche boundaries with flat interiors) rather than the smooth gradients an L2 penalty prefers. Registered as spatial loss"graph_tv"and applied to the niche latent over the per-sample in-batch graph; it is opt-in (TrainConfig.spatial_loss_weight > 0). Note that random mini-batches contain few true spatial edges, so a spatial-contiguous sampler would strengthen this term (not built here).- Parameters:
z (torch.Tensor) – Latent vectors, shape
(B, D).coords (torch.Tensor) – Cell coordinates in microns, shape
(B, 2).k (int, default=6) – Nearest neighbors per cell (excluding itself). Capped at
B - 1.
- Returns:
Scalar mean L1 total-variation penalty over neighbor pairs.
- Return type:
- nicheverse.losses.codebook_consistency(z, coords, k=6, margin=0.5)#
Margin hinge penalty on latent distance between spatial neighbors.
Computes the Euclidean distance between each cell’s latent and each of its
kspatial neighbors and penalizes only the amount by which that distance exceedsmargin(a hinge / ReLU). This is a softer, tolerance-banded version oflaplacian_smoothness(): neighbors are allowed to differ up tomarginfree of charge, so genuine local heterogeneity (e.g. a tumor boundary) is not over-smoothed, while grossly divergent neighbors are pulled together to stabilize the codebook assignment of adjacent cells.- Parameters:
z (torch.Tensor) – Latent vectors, shape
(B, D).coords (torch.Tensor) – Cell coordinates in microns, shape
(B, 2).k (int, default=6) – Spatial neighbors per cell. Valid range
>= 1.margin (float, default=0.5) – Distance tolerance below which neighbor pairs incur no penalty. Larger values permit more local variation. Must be
>= 0.
- Returns:
Scalar mean hinge penalty over neighbor pairs.
- Return type:
- nicheverse.losses.laplacian_smoothness(z, coords, k=6)#
Graph-Laplacian smoothness penalty on latents across spatial neighbors.
Penalizes the mean squared Euclidean difference between each cell’s latent and those of its
kspatial nearest neighbors (STAGATE-style Laplacian regularization). Minimizing it drives spatially adjacent cells toward similar embeddings, which in turn makes them more likely to quantize to the same cell state code. It is unbounded below only trivially (constantzgives 0), so it is used with a small weight alongside the reconstruction objective.- Parameters:
z (torch.Tensor) – Latent vectors, shape
(B, D).coords (torch.Tensor) – Cell coordinates in microns, shape
(B, 2), used to build the intra-batch neighbor graph.k (int, default=6) – Number of nearest neighbors per cell (excluding itself). Effectively capped at
B - 1. Valid range>= 1.
- Returns:
Scalar smoothness penalty (mean over cells and neighbors).
- Return type:
- nicheverse.losses.spatial_contrastive(z, coords, k=6, temperature=0.07, num_negatives=64)#
InfoNCE contrastive loss pulling spatial neighbors together, pushing negatives apart.
L2-normalizes the latents, then for each cell forms a positive score as the mean cosine similarity to its
kspatial neighbors and negative scores againstnum_negativesrandomly sampled in-batch cells, all scaled by1 / temperature, and returns the mean InfoNCE loss. Unlikelaplacian_smoothness()it is collapse-resistant: the negatives prevent the encoder from mapping everything to one point, so it shapes a spatially organized yet still discriminative latent space for the cell quantizer.- Parameters:
z (torch.Tensor) – Latent vectors, shape
(B, D).coords (torch.Tensor) – Cell coordinates in microns, shape
(B, 2).k (int, default=6) – Spatial neighbors treated as positives. Valid range
>= 1.temperature (float, default=0.07) – Softmax temperature on the similarity logits; lower sharpens the contrast. Must be
> 0.num_negatives (int, default=64) – Number of random in-batch negatives sampled per cell (sampled with replacement, so a positive may occasionally recur as a negative).
- Returns:
Scalar InfoNCE loss averaged over cells.
- Return type:
Annotation#
Turn learned codes into labeled cell states and spatial niches, grounded in
per-code marker and DEG evidence and, optionally, primary literature. The
end-to-end harness is annotate_codebook(), which
builds the evidence, proposes labels, gates them against markers and lab rules,
scores calibration, and returns an AnnotationResult.
The individual stages (code_evidence(),
annotate_codes(), the verify and evaluate helpers) are also
callable on their own.
Harness#
- nicheverse.annotate.annotate_codebook(adata, code_col, *, config=None, kind='cell', celltype_col=None, groundtruth_col=None, out_dir=None, run_meta=None, resolver=None, lineage_map=None)#
Run the full labeler -> gate -> refuter -> reconcile -> review -> provenance pipeline.
- Parameters:
adata – AnnData and the
obscolumn holding the code index (cell or niche codes).code_col (
str) – AnnData and theobscolumn holding the code index (cell or niche codes).config (
Optional[AnnotationConfig], default:None) –AnnotationConfig; setconfig.contextto aProjectContextto ground labels and supply the allowed vocabulary.config.refutertoggles the adversarial pass;config.min_marker_precisionandconfig.confidence_review_thresholdtune the gate and the review split.kind (
str, default:'cell') –"cell"(default) or"niche". For niches,celltype_col(cell-level labels) is required for the composition-primary evidence.celltype_col (
Optional[str], default:None) – Cell-type label column used to build niche composition (kind='niche'only).groundtruth_col (
Optional[str], default:None) – Optional independent label column; when given, each code is scored against its majority ground-truth label and the run is calibrated.out_dir (
Optional[str], default:None) – When given, the provenance manifest + scorecard CSV are written here.run_meta (
Optional[dict], default:None) – Run-level metadata recorded verbatim in the manifest (model, seed, timestamp, …).resolver (default:
None) – Injectable citation resolver(id) -> record|Nonehanded toverify.gate()(keeps citation checks offline in tests).lineage_map (
Optional[dict], default:None) – Optional label -> lineage (or(lineage, depth)) map forevaluate.score_code().
- Return type:
- class nicheverse.annotate.AnnotationResult(labels_df, review_df, scorecard_df=None, calibration=None, manifest_path=None, records=<factory>, kind='cell')#
Bases:
objectEverything the harness produces for one codebook annotation run.
- labels_df#
One row per code:
final_label,compartment,confidence(post-penalty),passed(gate),refuter_agree, andflags.
- review_df#
Subset of codes that did not auto-accept (gate failed, refuter disagreed, or low confidence), with a
review_reasoncolumn, for manual sign-off.
- scorecard_df#
Per-code grading table (from
evaluate.score_code()) when a ground-truth column was supplied, elseNone.
- calibration#
Confidence-vs-correctness calibration dict when scored, else
None.
- manifest_path#
Path to the provenance manifest JSON when
out_dirwas given, elseNone.
- records#
The per-code provenance records (kept in memory for inspection / re-use).
- kind#
"cell"or"niche".
- Parameters:
- class nicheverse.annotate.AnnotationConfig(provider='anthropic', model=None, tissue='', with_literature=False, context_cols=(), marker_context='cell type marker', top_markers=25, top_degs=20, refine=True, cluster_context=False, api_key=None, context=None, refuter=True, n_candidates=3, min_marker_precision=0.5, confidence_review_threshold=0.6)#
Bases:
objectSettings for
annotate_codes().- Parameters:
provider (
str, default:'anthropic') – LLM backend (anthropic/openai/ollama) and model id.model (
Optional[str], default:None) – LLM backend (anthropic/openai/ollama) and model id.tissue (
str, default:'') – Free-text tissue / disease context (e.g. “human clear cell RCC, brain metastasis”).with_literature (
bool, default:False) – IfTrue, search PubMed for the top markers of each code and include the hits.context_cols (
tuple[str,...], default:()) –obscolumns to summarize per code (e.g.("site_class", "sample_id")).refine (
bool, default:True) – Run a second pass that reconciles labels across codes (resolves duplicates / conflicts).cluster_context (
bool, default:False) – Hierarchically cluster codes first and give the reconciliation pass that structure, so similar codes get consistent compartments and distinguishing qualifiers.marker_context (
str, default:'cell type marker')top_markers (
int, default:25)top_degs (
int, default:20)refuter (
bool, default:True)n_candidates (
int, default:3)min_marker_precision (
float, default:0.5)confidence_review_threshold (
float, default:0.6)
Project context and priors#
- class nicheverse.annotate.ProjectContext(name='', species='', disease='', tissue='', platform='', panel='', sites=<factory>, expected_cell_types=<factory>, expected_niches=<factory>, site_col='', patient_col='', context_cols=<factory>, references=<factory>, notes='')#
Bases:
objectStructured, user-supplied context grounding LLM codebook annotation.
All fields are optional.
platformis validated on construction: a known non-IMST platform raisesValueError(nicheverse is imaging-based only), an unknown non-empty platform warns but is kept, and an empty platform is allowed.- Parameters:
name (
str, default:'')species (
str, default:'')disease (
str, default:'')tissue (
str, default:'')platform (
str, default:'')panel (
str, default:'')expected_cell_types (
list[CellTypePrior], default:<factory>)expected_niches (
list[NichePrior], default:<factory>)site_col (
str, default:'')patient_col (
str, default:'')notes (
str, default:'')
- classmethod from_dict(d)#
Build from a plain dict, tolerating unknown keys and coercing nested priors.
Robust to messy YAML:
None-> an empty context; unknown top-level keys are dropped;expected_cell_types/expected_nichesmay be given as dataclass instances, plain dicts, or any mix; a malformed nested entry (not a dict and not the matching dataclass, e.g. a bare string) is skipped rather than raising.- Parameters:
- Return type:
- classmethod from_yaml(path)#
Load from a YAML file via
yaml.safe_load(), thenfrom_dict().- Parameters:
path (
str)- Return type:
- to_prompt_block(max_cell_types=12, max_niches=8, max_markers=8)#
Render a compact, deterministic plain-text context block for an LLM prompt.
Omits empty fields, truncates long lists gracefully, and stays well under ~1500 chars for modest inputs. Uses
label: valuelines and numbered items (no markdown-bullets). The rendered block is guaranteed free of em-dashes and of leading markdown bullet markers (-,*,+followed by a space) even when user-supplied field values (names, markers, notes, references) contain them:_clean()rewrites those characters so the block always complies with the repo’s prose rules. Stored field values are never mutated (only the rendered copy is), so (de)serialization roundtrips are unaffected.
Evidence and labeling#
- nicheverse.annotate_codes(adata, code_col, config=None, **kwargs)#
Annotate every code in
adata.obs[code_col]with an LLM, grounded in evidence.Returns a DataFrame indexed by code with
label/compartment/confidence/rationale/key_markers/citations(andlabel_refinedwhenconfig.refine).- Parameters:
adata (
AnnData)code_col (
str)config (
Optional[AnnotationConfig], default:None)
- Return type:
DataFrame
- nicheverse.code_evidence(adata, code_col, *, extra_cols=(), top_markers=30, top_degs=30, layer=None)#
Assemble per-code evidence for annotation.
- Parameters:
adata (
AnnData) – AnnData carrying the code assignment inobs[code_col].code_col (
str) – Column ofobswith the code index (e.g.cell_codebook_idx).extra_cols (
tuple[str,...], default:()) –obscolumns to summarize the per-code distribution over (e.g.("site_class", "sample_id")).top_markers (
int, default:30) – Number of top markers per code (ranked by z-score across codes).top_degs (
int, default:30) – Number of one-vs-rest DEGs per code (rank_genes_groups).layer (
Optional[str], default:None) – Optionallayerskey to read expression from; defaults toX.
- Returns:
{code: {code, n_cells, frac, top_markers, top_degs, dist_<col>}}.top_markersis a list of(gene, z);top_degsa list of(gene, log2fc, padj).- Return type:
- nicheverse.annotate.annotate_niches(adata, niche_col, celltype_col, config=None, **kwargs)#
Annotate spatial-niche codes by their cell-type composition with an LLM.
Requires
celltype_col(cell-level labels, e.g. fromannotate_codes()). Returns a DataFrame indexed by niche code withlabel/dominant_types/confidence/rationale/composition.- Parameters:
adata (
AnnData)niche_col (
str)celltype_col (
str)config (
Optional[AnnotationConfig], default:None)
- Return type:
DataFrame
- nicheverse.annotate.niche_evidence(adata, niche_col, celltype_col, *, extra_cols=(), top_markers=20, top_compositions=8)#
Per-niche evidence: the cell-type composition and enriched genes of each spatial niche.
A niche (neighborhood code) is characterized by the community of cell types that co-occur in it, so annotation needs
celltype_col(typically the labels produced by annotating the cell codes) alongside marker enrichment.Returns
{niche: {code, n_cells, frac, composition [(cell_type, fraction)], top_markers [(gene, z)], dist_<col>}}.
- nicheverse.annotate.cluster_codes(adata, code_col, *, n_clusters=None, layer=None)#
Hierarchically group codes by mean-expression correlation.
Clusters similar codes together (correlation distance + average linkage) so they can be reviewed and annotated coarse-to-fine, as in the manual codebook pipeline.
n_clustersdefaults to about one cluster per eight codes.Returns a DataFrame indexed by code with an integer
clustercolumn.
- nicheverse.annotate.code_context(adata, code_col, *, patient_col=None, context_cols=())#
Per-code patient / specimen context.
One row per code with
n_cells,frac, the number of distinct patients (ifpatient_colgiven), and for each column incontext_colsthe dominant value and its fraction. This is the patient/specimen-context step of the codebook-review pipeline. Robust to absent columns (silently skipped).Returns a DataFrame indexed by code with columns
n_cells, frac, optionaln_patients, and per context col<col>_dominant+<col>_dominant_frac.
- nicheverse.annotate.code_groundtruth_concordance(adata, code_col, groundtruth_col)#
Cross-check codes against an independent ground-truth clustering/annotation.
Compares the learned codes in
obs[code_col]to a separately-derived label column (e.g. a Leiden clustering or a manual annotation) inobs[groundtruth_col]. This is the Leiden cross-check step of the codebook-review pipeline.- Parameters:
- Returns:
(crosstab, majority).crosstabis a DataFrame of code (rows) x ground-truth group (columns) normalized so each code row sums to ~1 (rows for codes with no valid ground-truth cells are all zero).majorityhas one row per code with columnscode, majority_group, majority_frac, n_cells.- Return type:
- nicheverse.annotate.write_evidence_bundle(adata, code_col, out_dir, *, groundtruth_col=None, patient_col=None, context_cols=(), top_markers=30, top_degs=30, layer=None)#
Compute the full per-code evidence bundle and dump it as CSVs.
Mirrors the documented 8-part codebook-review pipeline: per-code mean expression, z-score across codes, top markers, one-vs-rest DEGs, patient/specimen context, hierarchical code clustering, and (when a ground-truth column is given) the ground-truth crosstab + per-code majority.
- Parameters:
adata (
AnnData) – AnnData carrying the code assignment inobs[code_col].code_col (
str) – Column ofobswith the code index.out_dir (
str) – Directory to write CSVs into (created if absent).groundtruth_col (
Optional[str], default:None) – Optional independent label column for the Leiden/ground-truth cross-check.patient_col (
Optional[str], default:None) – Optional column counted forn_patientsin the context table.context_cols (
tuple[str,...], default:()) –obscolumns to summarize (dominant value) in the context table.top_markers (
int, default:30) – Numbers passed through tocode_evidence().top_degs (
int, default:30) – Numbers passed through tocode_evidence().layer (
Optional[str], default:None) – Optionallayerskey for expression; defaults toX.
- Returns:
Mapping of artifact name -> written CSV path.
- Return type:
- nicheverse.annotate.attach_labels(adata, code_col, labels, key_added='celltype_annot')#
Map a
code -> labelmapping (dict or annotate_codes DataFrame) ontoobs[key_added].
- nicheverse.annotate.code_dotplot(adata, code_col, *, top_n=5, genes=None, save_path=None, layer=None, font_size=11)#
Plot each code’s top markers as a dot grid and save a vector PDF.
- Parameters:
adata (
AnnData) – AnnData and theobscolumn holding the code index.code_col (
str) – AnnData and theobscolumn holding the code index.top_n (
int, default:5) – Top markers per code (by z-score) to include whengenesis not given.genes (
Optional[list[str]], default:None) – Explicit gene list to show; otherwise the union of each code’s top markers.save_path (
Union[str,Path,None], default:None) – Output.pdfpath. If omitted, the Matplotlib figure is returned.font_size (
int, default:11)
Verification and scoring#
The verify stage checks proposed labels against the evidence and lab rules; the evaluate stage scores per-code agreement and calibrates confidence.
- nicheverse.annotate.gate(label_dict, code_evidence, project_context, *, kind='cell', resolver=None, adata=None, code=None, z_thresh=1.0, min_marker_precision=0.5)#
Run every verification gate and synthesize an accept / penalize / reject decision.
POLICY. Exactly three failing CATEGORIES are evaluated, each contributing one flag and a fixed 0.34 to
confidence_penalty:MARKER. Markers were cited (
n_cited > 0) ANDmarker['precision'] < min_marker_precision– the LLM cited genes not enriched in this code (hallucination or segmentation leakage). Citing nothing is not itself a marker failure.CITATION. At least one citation has
resolved is False– a parseable id the INJECTED resolver could not retrieve, i.e. a likely fabricated identifier. An unresolved offline / unknown id (resolved is None) does NOT fail the gate, so the default (no resolver, offline) path never fails a citation merely for being unresolved. Anunparsedcitation also does not fail here.RULE.
apply_lab_rulesrecorded any violation (a site-restricted mislabel or an unsupported composite).
INVARIANTS.
confidence_penalty = min(1.0, 0.34 * n_failing_categories)andpassed is (confidence_penalty == 0.0)exactly; every failing category contributes exactly one entry toflags(rule violations are appended verbatim), so an emptyflagslist is equivalent topassed is True. The penalty caps at 1.0 so a caller can downweight rather than hard-drop borderline calls.adjusted_labelis the lab-rule-adjusted label when a rule fired, else the label’s vocabulary-closestwhen it is out of vocab and a plausible near-match exists, elseNone(leave the label as-is).A marker complaint about a NOVEL (out-of-vocab) label still fails the gate: the evidence must back whatever the LLM cited regardless of whether the label is in the vocabulary.
Returns:
{"passed": bool, "flags": [str,...], "marker": <marker_presence>, "citations": <check_citations>, "rules": <apply_lab_rules>, "vocab": <validate_vocabulary>, "adjusted_label": str|None, "confidence_penalty": float}
- nicheverse.annotate.marker_presence(cited_markers, code_evidence, *, z_thresh=1.0)#
Check which cited markers are actually enriched in the code’s evidence.
A cited marker is SUPPORTED if it appears in
code_evidence['top_markers']withz >= z_threshOR as a positive DEG incode_evidence['top_degs']. Markers that clear neither bar areabsent(hallucinated or segmentation leakage). Gene matching is case-insensitive.Returns
{"present": [...], "absent": [...], "precision": float, "n_cited": int}, whereprecision = len(present) / n_cited(and is1.0when nothing was cited, so an empty citation list is not itself penalized here).
- nicheverse.annotate.check_citations(citations, *, resolver=None, markers=None)#
Parse and (optionally) resolve each citation, flagging fabricated identifiers.
Each citation is parsed (see
_parse_citation()) for a DOI or a PMID.resolveris a callable(identifier_or_query) -> record | None; whenNoneit lazily falls back to PubMed vianicheverse.annotate.literature, but only if a network path is available. The resolver call is wrapped, so a resolver that raises (or an offline default with no network) yields aNonerecord rather than propagating – this function never raises. Tests inject a dict-backed fake resolver and never touch the network.Returns one dict per citation:
{"citation": raw, "id": parsed_or_None, "kind": "pmid"|"doi"|"unparsed", "resolved": bool|None, "supports": bool|None, "record": {...}|None}
resolvedsemantics (the key anti-hallucination distinction): :rtype:list[dict]None– no verdict: the id was unparsed, OR the DEFAULT (offline) resolver ran and could not reach the network. An unknown id is NEVER accused of being fabricated.True– an INJECTED or online resolver returned a record for the id.False– an INJECTED resolver ran and returned nothing for a parseable id: the id is a likely fabrication. Only the injected path can yieldFalse.
supportsisTrueiff a retrieved record’s text mentions one ofmarkersas a whole word, andNonewhenever no record was retrieved ormarkersis empty.
- nicheverse.annotate.apply_lab_rules(label_dict, code_evidence, project_context, *, adata=None, code=None)#
Encode the lab’s codebook conventions (
context.ANNOTATION_RULES) as executable checks.(a) Site-aware reassignment (rule 4): if the proposed label names a site-restricted (brain / CNS-only) type (matched as a whole word) but the code’s site distribution is not dominated by a permissive site – specifically the top-fraction site is non-permissive, OR a non-permissive site holds a material fraction (
>= _SITE_MATERIAL_FRAC = 0.25) of the code’s cells – record a violation and propose the closest general label. A permissive site holding a small non-permissive tail (plausible segmentation leakage) is NOT flagged. When the site distribution is unavailable the check is skipped and a note is recorded.(b) Composite label
X/Y(rule 5): allowed only if BOTH components have marker support clearing a relative bar (rel >= 1.3xcohort mean) and an absolute bar (abs >= 0.05) in the evidence. Absolute / relative-to-cohort expression is NOT recoverable from the per-code evidence dict (which carries only across-code z-scores and one-vs-rest DEGs, not the cohort-mean expression the exact bars need), so the check APPROXIMATES that support by requiring each component’s canonical markers (or, absent priors, the component name itself) to be present viamarker_presence(); this approximation is recorded innotes. A composite with fewer than two supported components collapses to its dominant component (most present markers, then higher precision). A site reassignment from (a) takes precedence over the collapsed label.Returns
{"violations": [...], "adjusted_label": str|None, "notes": [...]}.adjusted_labelisNonewhen no rule fired (i.e. the label is left as-is).
- nicheverse.annotate.validate_vocabulary(label, project_context, *, kind='cell', allow_novel=True)#
Check a label against the project’s expected vocabulary with a novelty escape hatch.
For
kind='cell'the allowed set isproject_context.expected_cell_typesnames; forkind='niche'it isproject_context.expected_nichesnames (any otherkindis treated as'cell').in_vocabis set by an EXACT (case- and whitespace-insensitive) name match ONLY, so fuzzy matching can never manufacture a false in-vocab hit.closestsurfaces the nearest allowed label (see_closest()), gated byallow_novel:allow_novel=True(default): only surfaceclosestwhen it is a plausible near-match (similarity>= 0.6); an out-of-vocab label with no near neighbor returnsclosest=Noneand is left untouched by the caller (novel / uncertain codes are allowed, not errors).allow_novel=False: novel labels are not tolerated, soclosestis ALWAYS the nearest allowed label (when any vocabulary exists), letting the caller coerce an out-of-vocab label onto the closest expected type.
An exact in-vocab match always returns itself as
closest. Returns{"in_vocab": bool, "closest": str|None, "kind": kind}.
- nicheverse.annotate.score_code(proposed, reference_label, code_evidence, *, lineage_map=None, gate_result=None, z_thresh=1.0, k=15)#
Score one proposed code label against an independent reference label.
- Parameters:
proposed (
dict) – The LLM’s proposed-label dict, e.g.{"label", "compartment", "confidence", "key_markers", "citations", ...}.key_markersmay be a list or a;/,-joined string; missing keys are tolerated.reference_label (
str) – Independent ground-truth label for the code (e.g. the majority Leiden group fromcode_groundtruth_concordance()). May be None/empty (thenlabel_matchis None).code_evidence (
dict) – Per-code evidence dict fromnicheverse.annotate.code_evidence()(top_markers=[(gene, z)], optionaltop_degs,dist_<col>keys).lineage_map (
Optional[dict], default:None) – Optional label -> broad lineage (or label ->(lineage, depth)) map that approximates the Cell Ontology tiers. See the module docstring.gate_result (
Optional[dict], default:None) – Optional dict from a decoupled gate (verify.gate), carrying marker precision, citation resolution, and rule violations. Passed in so this module never imports verify. Recognized keys (all optional):rules_pass(bool) orrule_violations(list);n_absent_markers/absent_markers;n_unresolved_citations/unresolved_citations;marker_precision.z_thresh (
float, default:1.0) – Atop_markersgene counts as enriched when its z-score >= this.k (
int, default:15) – Marker precision/recall are computed against the code’s top-kmarkers.
- Returns:
Flat scorecard with the dimensions (A)
label_match(1.0/0.5/0.0/None), (B)granularity(“over_call”/”under_call”/”match”/None), (C)marker_precision/marker_recall, (D)rule_compliance(“pass”/”fail”), (E) hallucination countsn_absent_markers/n_unresolved_citations, plus the raw proposed label, reference, compartment, confidence, code, and anyplatformcarried on the proposed dict.- Return type:
- nicheverse.annotate.calibration(scorecards, *, n_bins=4)#
Does the LLM’s stated confidence predict whether it was right?
Bins scorecards by
confidenceand reports the meanlabel_matchper bin, plus the Spearman rank correlation between confidence andlabel_match. Records with a missing confidence or a Nonelabel_matchare dropped from the correlation (and binning) but counted inn_total.Returns
{"spearman": float|None, "bins": {bin: {...}}, "n": int, "n_total": int}. A positive Spearman means higher confidence tracks higher correctness.
- nicheverse.annotate.scorecard_table(scorecards)#
One row per code with every scorecard dimension (a tidy grading table).
- nicheverse.annotate.write_provenance_manifest(out_dir, run_meta, per_code_records)#
Persist an auditable, replayable run manifest as JSON (+ a scorecard CSV).
The manifest captures
run_metaverbatim (model id, seed, temperature, ProjectContext summary, lab-rule version, and a caller-suppliedtimestamp– this function never callsdatetime.now) and, per code, an evidence hash, a prompt hash, the raw LLM output, the gate result, the citations, and the final label, so any label can be audited or replayed. Ascorecardsarray is also embedded when the records carry scorecard fields, and the scorecard table is dumped toprovenance_scorecards.csvnext to the manifest.- Parameters:
out_dir (
str) – Directory for the manifest / CSV (created withexist_ok=True).run_meta (
dict) – Run-level metadata (see above). Copied verbatim into the manifest.per_code_records (
list[dict]) – One dict per code. Any ofcode,evidence(hashed if present, else a precomputedevidence_hashis used),prompt(->prompt_hashor a precomputed one),raw_output/output,gate_result,citations, andfinal_label/labelare recorded; scorecard fields pass through.
- Returns:
Absolute path to the written manifest JSON.
- Return type:
Literature and LLM providers#
- nicheverse.annotate.pubmed_search(query, *, max_results=4, api_key=None)#
Return up to
max_resultsPubMed hits{pmid, title, journal, year, first_author}.
- nicheverse.annotate.biorxiv_search(query, *, max_results=3)#
Placeholder: the public bioRxiv API has no keyword-search endpoint, so this returns [].
Use
pubmed_search()for literature grounding (PubMed also indexes many preprints).
- nicheverse.annotate.literature_for_markers(markers, *, context='cell type marker', max_results=3, api_key=None)#
Map each marker gene to its top PubMed references for
context.
- nicheverse.annotate.call_llm(prompt, *, provider='anthropic', model=None, api_key=None, system=None, max_tokens=1200, temperature=0.0)#
Send
promptto an LLM and return the raw text response.- Parameters:
provider (
str, default:'anthropic') –"anthropic"(Claude),"openai"(GPT), or"ollama"(a local Ollama server; also covers any OpenAI-compatible local endpoint).model (
Optional[str], default:None) – Model id; defaults per provider (Claude Opus / GPT-4o / llama3).api_key (
Optional[str], default:None) – Overrides theANTHROPIC_API_KEY/OPENAI_API_KEYenvironment variable.system (
Optional[str], default:None) – Optional system prompt.prompt (
str)max_tokens (
int, default:1200)temperature (
float, default:0.0)
- Return type:
Plotting#
Vector PDF plots: codebook usage, per-sample spatial code maps, training losses.
This module sets a few sensible matplotlib defaults at import time
(pdf.fonttype = 42 for editable text, font family Arial when available,
default font size 16). We only switch the backend to Agg when no display
is available; in Jupyter the user’s chosen backend is preserved.
- nicheverse.plotting.codebook_usage_pdf(cell_idx, neigh_idx, save_path)#
Save a side-by-side bar chart of cell-code and neighborhood-code usage.
- nicheverse.plotting.per_sample_spatial_pdf(spatial, sample_ids, cell_idx, neigh_idx, save_dir)#
Write one PDF per sample with cell-code and neighborhood-code spatial maps.
- Parameters:
spatial (
ndarray) –(n_cells, 2)micron coordinates.sample_ids (
ndarray) –(n_cells,)sample label per cell.cell_idx (
ndarray) – Integer code assignments per cell.neigh_idx (
ndarray) – Integer code assignments per cell.save_dir (
str|Path) – Directory to write the per-sample PDFs into. Created if missing.
- Returns:
The output directory.
- Return type:
Path
- nicheverse.plotting.training_loss_pdf(losses, save_path)#
Save the per-epoch total / cell / neighborhood loss curves.
Utilities#
Determinism, environment capture, and hashing for byte-exact reproduction.
- class nicheverse.Keys#
Bases:
objectNamespaced AnnData keys (see
anndata).-
CELL_CODE:
Final= 'cell_codebook_idx'# obscolumn with the cell-codebook index (cell state).
-
NEIGHBORHOOD_CODE:
Final= 'neighborhood_codebook_idx'# obscolumn with the neighborhood-codebook index (spatial niche).
-
SAMPLE:
Final= 'sample_id'# obscolumn with the per-sample identifier used for the spatial graph.
-
CELL_EMBEDDING:
Final= 'X_cell_embedding'# obsmkey with the continuous per-cell embedding (pre-quantization).
-
NEIGHBORHOOD_EMBEDDING:
Final= 'X_neighborhood_embedding'# obsmkey with the continuous per-neighborhood embedding.
-
SPATIAL:
Final= 'spatial'# obsmkey with spatial coordinates in microns (x, y).
-
CELL_CODE:
- nicheverse.seed_everything(seed=9, deterministic=True)#
Seed Python, NumPy, and PyTorch (CPU + CUDA) for reproducible runs.
- Parameters:
seed (
int, default:9) – Integer seed applied torandom,numpy,torch.manual_seed, andtorch.cuda.manual_seed_all.deterministic (
bool, default:True) – If True, request deterministic cuDNN ops and calltorch.use_deterministic_algorithms(True, warn_only=True). If any op the model uses lacks a deterministic kernel, a warning will be emitted at runtime; the run will still complete, but bit-for-bit reproducibility is not guaranteed.
- Return type:
Notes
For full bit-identical CUDA runs, the environment variable
CUBLAS_WORKSPACE_CONFIG=:4096:8must be set BEFORE the firsttorchimport in the process. This function sets it inos.environfor completeness, but iftorchwas already imported, setting the variable here has no effect on cuBLAS for the current process. To guarantee the setting takes effect, export it in your shell or set it before launching Python (e.g.CUBLAS_WORKSPACE_CONFIG=:4096:8 python ...). SeeDETERMINISM.mdfor the full reproducibility contract.
- nicheverse.env_snapshot()#
Capture Python, OS, key library versions, CUDA info, and (best-effort) git SHA.
- Returns:
A JSON-safe dict suitable for
json.dumps. Missing or unimportable libraries appear asNone.- Return type:
- nicheverse.write_env_snapshot(path)#
Write
env_snapshot()topath(as JSON) and returnpathasPath.
- nicheverse.sha256_array(arr)#
Stable SHA256 of an ndarray’s content, dtype, and shape.
Hashes
str(dtype),str(shape), thentobytes()of the contiguous array. Two arrays with different dtypes but identical content intentionally produce different hashes.
- nicheverse.sha256_file(path, block=1048576)#
SHA256 of a file, streamed in
block-sized chunks (default 1 MiB).