Skip to content

Commit aabcee6

Browse files
attribution new file added, type check ignoring vmap due to pytorch / pyright conflict
1 parent 6ea0baa commit aabcee6

2 files changed

Lines changed: 88 additions & 1 deletion

File tree

src/glass_box_umap/attribution.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""Gene-space attribution utilities for scRNA-seq data.
2+
3+
Maps Jacobians from PCA space to gene space and computes per-gene
4+
contributions to the UMAP embedding.
5+
"""
6+
7+
from __future__ import annotations
8+
from typing import Any
9+
10+
import numpy as np
11+
from numpy.typing import NDArray
12+
13+
try:
14+
import anndata as ad
15+
except ImportError: # pragma: no cover
16+
ad = None # type: ignore[assignment]
17+
18+
19+
def compute_gene_contributions(
20+
J: NDArray[np.floating],
21+
adata: Any, # anndata.AnnData; full stubs pending scverse/anndata#2173
22+
) -> tuple[NDArray[np.floating], NDArray[np.floating], NDArray[np.str_]]:
23+
"""Map Jacobian from PCA space to gene space and compute per-gene contributions.
24+
25+
Uses the chain rule to project the PCA-space Jacobian through the PCA
26+
loadings matrix, then multiplies by mean-centered HVG expression to get
27+
each gene's additive contribution to the embedding.
28+
29+
Args:
30+
J: Jacobian in PCA space, shape ``(n, out_dim, n_pcs)``.
31+
adata: AnnData object with ``.var.highly_variable``, ``.varm["PCs"]``,
32+
and ``adata.uns["pca_mean_hvg"]`` populated by preprocessing.
33+
34+
Returns:
35+
A tuple of:
36+
- ``contributions_gene``: shape ``(n, out_dim, n_hvgs)``
37+
- ``importance_gene``: L2 norm over embedding dims, shape ``(n, n_hvgs)``
38+
- ``gene_names_hvg``: array of HVG gene names
39+
"""
40+
hvg_mask = adata.var.highly_variable.values
41+
W_hvg = adata.varm["PCs"][hvg_mask, :] # (n_hvgs, n_pcs)
42+
gene_names_hvg = adata.var_names[hvg_mask].values
43+
44+
# Jacobian w.r.t. HVG expression: J_gene = J_pc @ W_hvg.T
45+
J_gene = np.einsum("noi,gi->nog", J, W_hvg) # (n, out_dim, n_hvgs)
46+
47+
# Gene-space input (mean-centered HVG expression)
48+
X_hvg = adata[:, hvg_mask].X
49+
if hasattr(X_hvg, "toarray"):
50+
X_hvg = X_hvg.toarray()
51+
pca_mean = adata.uns["pca_mean_hvg"]
52+
X_hvg_centered = X_hvg - pca_mean
53+
54+
# Per-gene contributions and importance
55+
contributions_gene = np.einsum(
56+
"nog,nog->nog", J_gene, X_hvg_centered[:, np.newaxis, :]
57+
) # (n, out_dim, n_hvgs)
58+
importance_gene = np.linalg.norm(contributions_gene, axis=1) # (n, n_hvgs)
59+
60+
return contributions_gene, importance_gene, gene_names_hvg
61+
62+
63+
def verify_gene_reconstruction(
64+
contributions_gene: NDArray[np.floating],
65+
Z: NDArray[np.floating],
66+
tol: float = 1e-3,
67+
) -> float:
68+
"""Verify that gene-space contributions sum to the embedding.
69+
70+
Args:
71+
contributions_gene: shape ``(n, out_dim, n_hvgs)``.
72+
Z: Embedding, shape ``(n, out_dim)``.
73+
tol: Relative error threshold for PASS/FAIL.
74+
75+
Returns:
76+
Relative max error.
77+
"""
78+
Z_reconstructed = contributions_gene.sum(axis=2)
79+
max_err = np.abs(Z_reconstructed - Z).max()
80+
mean_err = np.abs(Z_reconstructed - Z).mean()
81+
rel_err = max_err / (np.abs(Z).max() + 1e-8)
82+
print("\n── Gene-Space Reconstruction ──")
83+
print(f" Max error : {max_err:.2e}")
84+
print(f" Mean error : {mean_err:.2e}")
85+
print(f" Rel error : {rel_err:.2e}")
86+
print(f" Verification {'PASSED ✓' if rel_err < tol else 'FAILED ✗'}")
87+
return rel_err

src/glass_box_umap/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import torch
66
import torch.nn as nn
77
from numpy.typing import NDArray
8-
from torch.func import functional_call, jacrev, vmap
8+
from torch.func import functional_call, jacrev, vmap # type: ignore[reportPrivateImportUsage]
99

1010
from glass_box_umap.components import DeepPReLUNet
1111
from glass_box_umap.parametric_umap.registry import register_encoder

0 commit comments

Comments
 (0)