Skip to content

Commit 6ea0baa

Browse files
vmap fast computation of jacobian, pca contributions mapped to original features, graph filtering improved, encoder model improved, gene exp embeddings example added, mnist example updated with features
1 parent b691fcc commit 6ea0baa

15 files changed

Lines changed: 648 additions & 110 deletions

File tree

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,9 @@ venv/
6464

6565
# OS
6666
.DS_Store
67+
68+
# Images
69+
*.png
70+
71+
# Lightning logs
72+
*tmp*

.pre-commit-config.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,24 +18,24 @@ repos:
1818
hooks:
1919
- id: format
2020
name: Format
21-
entry: make format
21+
entry: uv run make format
2222
language: system
2323
pass_filenames: false
2424

2525
- id: lint
2626
name: Lint
27-
entry: make lint
27+
entry: uv run make lint
2828
language: system
2929
pass_filenames: false
3030

3131
- id: type-check
3232
name: Type check
33-
entry: make typecheck
33+
entry: uv run make typecheck
3434
language: system
3535
pass_filenames: false
3636

3737
- id: test
3838
name: Test
39-
entry: make test
39+
entry: uv run make test
4040
language: system
4141
pass_filenames: false

scripts/utils_gene_exp.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
from pathlib import Path
2+
import copy
3+
import time
4+
import os
5+
import subprocess
6+
7+
import numpy as np
8+
9+
import matplotlib.pyplot as plt
10+
import matplotlib.cm as cm
11+
from matplotlib.colors import ListedColormap
12+
from matplotlib.markers import MarkerStyle
13+
from scipy.stats import spearmanr
14+
15+
import torch
16+
import torch.nn as nn
17+
from torch.func import functional_call, vmap, jacrev
18+
19+
import scanpy as sc
20+
import anndata as ad
21+
22+
from glass_box_umap import ParametricUMAP
23+
24+
# ═══════════════════════════════════════════════════════════════════════════════
25+
# Utils — move to utils.py and `from utils import *`
26+
# ═══════════════════════════════════════════════════════════════════════════════
27+
28+
# ── Data loading & preprocessing ──────────────────────────────────────────────
29+
30+
def download_bone_marrow_data(
31+
url="ftp://ftp.ncbi.nlm.nih.gov/geo/series/GSE194nnn/GSE194122/suppl/"
32+
"GSE194122_openproblems_neurips2021_cite_BMMC_processed.h5ad.gz",
33+
filename="GSE194122_openproblems_neurips2021_cite_BMMC_processed.h5ad.gz",
34+
) -> ad.AnnData:
35+
"""Download, unzip, and load the bone marrow dataset."""
36+
unzipped = filename.replace(".gz", "")
37+
if not os.path.isfile(unzipped):
38+
if not os.path.isfile(filename):
39+
subprocess.run(["wget", url, "--no-verbose"])
40+
subprocess.run(["gunzip", filename])
41+
return sc.read_h5ad(unzipped)
42+
43+
44+
def preprocess_adata(
45+
adata: ad.AnnData,
46+
min_genes: int = 100,
47+
min_cells: int = 3,
48+
n_top_genes: int = 2000,
49+
n_pcs: int = 50,
50+
batch_key: str = "Samplename",
51+
run_scrublet: bool = False,
52+
) -> ad.AnnData:
53+
"""Full scRNA-seq preprocessing: QC filtering, normalize, HVG selection, PCA."""
54+
print("--- Starting Preprocessing ---")
55+
56+
adata.obs_names_make_unique()
57+
adata.var_names_make_unique()
58+
59+
# Flag QC gene categories
60+
adata.var["mt"] = adata.var_names.str.startswith("MT-")
61+
adata.var["ribo"] = adata.var_names.str.startswith(("RPS", "RPL"))
62+
adata.var["hb"] = adata.var_names.str.contains("^HB[^(P)]")
63+
adata.var["technical"] = adata.var_names.str.startswith(
64+
("MALAT1", "NEAT1", "FOS", "JUN")
65+
)
66+
67+
sc.pp.calculate_qc_metrics(
68+
adata, qc_vars=["mt", "ribo", "hb", "technical"], inplace=True, log1p=True
69+
)
70+
71+
# Remove QC genes, filter cells/genes
72+
genes_to_remove = adata.var["mt"] | adata.var["ribo"] #| adata.var["technical"]
73+
adata._inplace_subset_var(~genes_to_remove)
74+
sc.pp.filter_cells(adata, min_genes=min_genes)
75+
sc.pp.filter_genes(adata, min_cells=min_cells)
76+
if run_scrublet:
77+
sc.pp.scrublet(adata, batch_key=batch_key)
78+
79+
# Normalize, log-transform, HVGs
80+
adata.layers["counts"] = adata.X.copy()
81+
sc.pp.normalize_total(adata)
82+
sc.pp.log1p(adata)
83+
84+
sc.pp.highly_variable_genes(adata, n_top_genes=n_top_genes, batch_key=batch_key)
85+
86+
# Now regress out on the full matrix (or subset to HVGs first)
87+
sc.pp.regress_out(adata, ['total_counts'])
88+
89+
# Store HVG mean for later gene-space reconstruction
90+
X_hvg = adata[:, adata.var.highly_variable].X
91+
if hasattr(X_hvg, "toarray"):
92+
X_hvg = X_hvg.toarray()
93+
adata.uns["pca_mean_hvg"] = X_hvg.mean(axis=0)
94+
95+
# PCA
96+
sc.tl.pca(adata, n_comps=n_pcs, use_highly_variable=True)
97+
98+
return adata
99+
100+
101+
def prepare_data(
102+
groupby_key: str = "cell_type",
103+
n_pcs: int = 50,
104+
batch_key: str = "Samplename",
105+
n_top_cell_types: int = 32,
106+
) -> ad.AnnData:
107+
"""Load, preprocess, and subset to top cell types."""
108+
adata_raw = download_bone_marrow_data()
109+
adata_raw.var_names_make_unique()
110+
111+
adata_processed = preprocess_adata(
112+
adata_raw, n_top_genes=2000, n_pcs=n_pcs, batch_key=batch_key
113+
)
114+
115+
top_types = adata_processed.obs[groupby_key].value_counts().nlargest(n_top_cell_types).index
116+
adata_final = adata_processed[adata_processed.obs[groupby_key].isin(top_types)].copy()
117+
118+
return adata_final
119+
120+
def print_top_genes_per_cluster(importance_gene, gene_names_hvg, labels, label_names, n_top=8):
121+
"""Print top contributing genes for each cluster."""
122+
print(f"\n── Top {n_top} Genes Per Cluster ──")
123+
for cluster_id in range(labels.max() + 1):
124+
mask = labels == cluster_id
125+
mean_imp = importance_gene[mask].mean(axis=0)
126+
top_genes = gene_names_hvg[np.argsort(mean_imp)[::-1][:n_top]]
127+
print(f" {label_names[cluster_id]}: {list(top_genes)}")
128+
129+
130+
# ── Plotting helpers ──────────────────────────────────────────────────────────
131+
132+
def make_tab40_cmap(seed=None):
133+
"""Create a 40-color colormap with a randomized order."""
134+
top = plt.get_cmap("tab20b").colors
135+
bottom = plt.get_cmap("tab20c").colors
136+
combined = np.vstack((top, bottom))
137+
138+
# Generate a random permutation
139+
rng = np.random.default_rng(seed)
140+
shuffled_indices = rng.permutation(len(combined))
141+
shuffled_colors = combined[shuffled_indices]
142+
143+
cmap = ListedColormap(shuffled_colors, name="tab40")
144+
# Optional: Register it if you want to call it by string later
145+
plt.colormaps.register(cmap=cmap)
146+
147+
return cmap
148+
149+
def plot_embedding(Z, labels, cmap, filename, title="Parametric UMAP"):
150+
"""Scatter plot of embedding colored by cell type."""
151+
fig, ax = plt.subplots(figsize=(10, 8))
152+
scatter = ax.scatter(
153+
Z[:, 0], Z[:, 1],
154+
c=labels,
155+
cmap=cmap,
156+
s=0.1,
157+
marker=MarkerStyle("o", fillstyle="full"),
158+
alpha=0.5,
159+
rasterized=True,
160+
)
161+
plt.colorbar(scatter, ax=ax, label="Cell Type")
162+
ax.set_title(title, fontsize=16)
163+
ax.set_xlabel("UMAP 1")
164+
ax.set_ylabel("UMAP 2")
165+
ax.grid(True, alpha=0.3)
166+
plt.savefig(filename, dpi=300, bbox_inches="tight")
167+
plt.close()
168+
169+
170+
def plot_top_gene_map(Z, top_gene_names, gene_names_hvg, filename, n_label=60):
171+
"""UMAP colored by each cell's top contributing gene, with centroid labels."""
172+
unique_genes, counts = np.unique(top_gene_names, return_counts=True)
173+
top_n_genes = unique_genes[np.argsort(counts)[::-1][:n_label]]
174+
cmap = cm.get_cmap("tab40", 40)
175+
176+
fig, ax = plt.subplots(figsize=(10, 10))
177+
178+
for i, gene in enumerate(top_n_genes):
179+
mask = top_gene_names == gene
180+
ax.scatter(
181+
Z[mask, 0], Z[mask, 1],
182+
s=0.3, alpha=0.7, color=cmap(i % 40),
183+
label=gene, rasterized=True,
184+
)
185+
186+
# Centroid labels
187+
for gene in top_n_genes:
188+
mask = top_gene_names == gene
189+
cx, cy = Z[mask, 0].mean(), Z[mask, 1].mean()
190+
ax.annotate(
191+
gene, (cx, cy),
192+
fontsize=7, fontweight="bold", ha="center", va="center",
193+
bbox=dict(boxstyle="round,pad=0.2", fc="white", ec="gray", alpha=0.85),
194+
)
195+
196+
ax.set_title("UMAP — colored by top contributing gene")
197+
ax.set_xlabel("UMAP 1")
198+
ax.set_ylabel("UMAP 2")
199+
ax.legend(markerscale=8, fontsize=7, loc="best", ncol=2)
200+
plt.tight_layout()
201+
plt.savefig(filename, dpi=150, bbox_inches="tight")
202+
plt.close()

0 commit comments

Comments
 (0)