| """regenerate fig5 (dingwall UMAP, 2 panels) + fig6 (3-panel multi UMAP).
|
|
|
| Uses shared canonical palette (scripts/figures/palette.py) so a class gets
|
| the same color in every figure. Reuses project()/do_umap()/_load_dahlin_raw()/
|
| _load_veres() from build_pca_vs_marker_umaps.py.
|
|
|
| Caches expensive UMAP embeddings to figures/_cache_*.npz for reuse.
|
|
|
| Fig 6c restricts Veres to the 12,297 held-out slice
|
| (data/corpus/pancreas/held_out_labeled/veres_GSE114412_test.h5ad).
|
|
|
| outputs:
|
| figures/fig5_dingwall_umap.pdf
|
| figures/fig6_multi_umap.pdf
|
| """
|
| from __future__ import annotations
|
| from pathlib import Path
|
| import warnings, sys, numpy as np, pandas as pd
|
| warnings.filterwarnings("ignore")
|
| import matplotlib; matplotlib.use("Agg")
|
| import matplotlib.pyplot as plt
|
| import anndata as ad
|
|
|
| import os as _os
|
| from pathlib import Path as _Path
|
| PANDA_ROOT = _Path(_os.environ.get("PANDA_ROOT", str(_Path(__file__).resolve().parents[2])))
|
| ROOT = Path(str(PANDA_ROOT))
|
| FIG = ROOT / "figures"
|
| FIG.mkdir(exist_ok=True)
|
|
|
| sys.path.insert(0, str(ROOT / "scripts/figures"))
|
| from build_pca_vs_marker_umaps import (
|
| project, do_umap, _load_dahlin_raw, _load_veres,
|
| )
|
| from palette import color_for, apply_style, GENOTYPE_COLORS, STAGE_COLORS
|
| apply_style()
|
|
|
|
|
| SUPTITLE_FS = 15
|
| TITLE_FS = 14
|
| LABEL_FS = 12
|
| TICK_FS = 11
|
| LEGEND_FS = 10
|
| LEGEND_TITLE_FS = 11
|
| ANNOT_FS = 11
|
| PANEL_FS = 13
|
|
|
|
|
| plt.rcParams.update({
|
| "axes.titlesize": TITLE_FS,
|
| "axes.labelsize": LABEL_FS,
|
| "xtick.labelsize": TICK_FS,
|
| "ytick.labelsize": TICK_FS,
|
| "legend.fontsize": LEGEND_FS,
|
| "legend.title_fontsize": LEGEND_TITLE_FS,
|
| "figure.titlesize": SUPTITLE_FS,
|
| })
|
|
|
| RS = 42
|
|
|
|
|
|
|
| _ISLET_GRAY = "#707070"
|
|
|
|
|
|
|
| _OTHER_GREY = "#909090"
|
|
|
|
|
| DINGWALL_CKO = {"GSM6833482", "GSM6833483"}
|
| DINGWALL_WT = {"GSM6833478", "GSM6833479", "GSM6833480", "GSM6833481"}
|
|
|
|
|
| def _panel_letter(ax, letter, x=-0.08, y=1.03, fontsize=PANEL_FS):
|
| ax.text(x, y, f"({letter})", transform=ax.transAxes,
|
| fontsize=fontsize, fontweight="bold", va="bottom", ha="left")
|
|
|
|
|
| def _dominant_classes(P, min_frac=0.005):
|
| """return the classes making up >= min_frac of the cells (in count order)."""
|
| from collections import Counter
|
| n = len(P)
|
| counts = Counter(P.tolist())
|
| return [c for c, k in counts.most_common() if k / n >= min_frac]
|
|
|
|
|
| def fig5_dingwall():
|
| cache_p = FIG / "_cache_dingwall_full_marker.npz"
|
| if cache_p.exists():
|
| print(f"[fig5] reusing cache {cache_p.name}", flush=True)
|
| c = np.load(cache_p, allow_pickle=True)
|
| emb, P, gt = c["emb"], c["P"], c["genotype"]
|
| n = len(emb)
|
| else:
|
| print("[fig5] loading dingwall raw ...", flush=True)
|
| raw = ad.read_h5ad(ROOT / "data/raw/GSE220977_combined.h5ad")
|
| gt = np.where(raw.obs["sample"].astype(str).isin(list(DINGWALL_CKO)), "En1-cKO",
|
| np.where(raw.obs["sample"].astype(str).isin(list(DINGWALL_WT)), "WT", "other"))
|
| n = raw.n_obs
|
| print(f"[fig5] projecting {n:,} cells (PANDA-Marker) ...", flush=True)
|
| Z, P, _ = project(raw, "pan_skin", "marker")
|
| print(f"[fig5] UMAP on {n:,} projections ...", flush=True)
|
| emb = do_umap(Z)
|
| np.savez(cache_p, emb=emb, P=P, genotype=gt)
|
|
|
| fig, axes = plt.subplots(1, 2, figsize=(18, 8), constrained_layout=True)
|
|
|
|
|
| ax = axes[0]
|
| kept = _dominant_classes(P, min_frac=0.005)
|
| P_r = np.where(np.isin(P, kept), P, "other")
|
|
|
| m_other = P_r == "other"
|
| if m_other.sum() > 0:
|
| ax.scatter(emb[m_other, 0], emb[m_other, 1], s=6, alpha=0.6,
|
| c=_OTHER_GREY, label=f"other (n={int(m_other.sum()):,})",
|
| linewidths=0, rasterized=True)
|
| for cls in kept:
|
| m = P_r == cls
|
| if m.sum() == 0: continue
|
| ax.scatter(emb[m, 0], emb[m, 1], s=6, alpha=0.6,
|
| c=color_for(cls),
|
| label=f"{cls} (n={int(m.sum()):,})", linewidths=0,
|
| rasterized=True)
|
| ax.set_title("Dingwall skin — PANDA-Marker predicted class",
|
| fontsize=TITLE_FS, pad=8)
|
| ax.set_xlabel("UMAP-1", fontsize=LABEL_FS)
|
| ax.set_ylabel("UMAP-2", fontsize=LABEL_FS)
|
| ax.set_xticks([]); ax.set_yticks([])
|
| ax.legend(bbox_to_anchor=(1.02, 1), loc="upper left", fontsize=LEGEND_FS,
|
| markerscale=1.6, frameon=False,
|
| title=f"Predicted class (n={n:,})",
|
| title_fontsize=LEGEND_TITLE_FS,
|
| handletextpad=0.5, borderaxespad=0.4)
|
| _panel_letter(ax, "a")
|
|
|
|
|
| ax = axes[1]
|
| for g in ["other", "WT", "En1-cKO"]:
|
| m = gt == g
|
| if m.sum() == 0: continue
|
| ax.scatter(emb[m, 0], emb[m, 1], s=6, alpha=0.6,
|
| c=GENOTYPE_COLORS.get(g, "#bbbbbb"),
|
| label=f"{g} (n={int(m.sum()):,})",
|
| linewidths=0, rasterized=True)
|
| ax.set_title("Dingwall skin — En1 genotype", fontsize=TITLE_FS, pad=8)
|
| ax.set_xlabel("UMAP-1", fontsize=LABEL_FS)
|
| ax.set_ylabel("UMAP-2", fontsize=LABEL_FS)
|
| ax.set_xticks([]); ax.set_yticks([])
|
| ax.legend(bbox_to_anchor=(1.02, 1), loc="upper left", fontsize=LEGEND_FS,
|
| markerscale=1.6, frameon=False,
|
| title="En1 genotype", title_fontsize=LEGEND_TITLE_FS,
|
| handletextpad=0.5, borderaxespad=0.4)
|
| _panel_letter(ax, "b")
|
|
|
| plt.savefig(FIG / "fig5_dingwall_umap.pdf", bbox_inches="tight", dpi=200)
|
| plt.close()
|
| print(f"[fig5] wrote {FIG}/fig5_dingwall_umap.pdf", flush=True)
|
|
|
|
|
| def _get_dingwall_cached():
|
| cache_p = FIG / "_cache_dingwall_full_marker.npz"
|
| if cache_p.exists():
|
| c = np.load(cache_p, allow_pickle=True)
|
| return c["emb"], c["genotype"]
|
|
|
| print("[fig6] recomputing dingwall ...", flush=True)
|
| raw = ad.read_h5ad(ROOT / "data/raw/GSE220977_combined.h5ad")
|
| gt_s = np.where(raw.obs["sample"].astype(str).isin(list(DINGWALL_CKO)), "En1-cKO",
|
| np.where(raw.obs["sample"].astype(str).isin(list(DINGWALL_WT)), "WT", "other"))
|
| Z_s, P_s, _ = project(raw, "pan_skin", "marker")
|
| emb_s = do_umap(Z_s)
|
| np.savez(cache_p, emb=emb_s, P=P_s, genotype=gt_s)
|
| return emb_s, gt_s
|
|
|
|
|
| def _get_dahlin_cached():
|
| cache_p = FIG / "_cache_dahlin_marker.npz"
|
| if cache_p.exists():
|
| print(f"[fig6] reusing {cache_p.name}", flush=True)
|
| c = np.load(cache_p, allow_pickle=True)
|
| return c["emb"], c["genotype"]
|
| print("[fig6] loading dahlin raw ...", flush=True)
|
| a_d = _load_dahlin_raw()
|
| gt_d = a_d.obs["genotype"].astype(str).values
|
| print(f"[fig6] projecting dahlin ({a_d.n_obs:,}) ...", flush=True)
|
| Z_d, _, _ = project(a_d, "hematopoiesis", "marker")
|
| print(f"[fig6] UMAP dahlin ...", flush=True)
|
| emb_d = do_umap(Z_d)
|
| np.savez(cache_p, emb=emb_d, genotype=gt_d)
|
| return emb_d, gt_d
|
|
|
|
|
| def _get_veres_heldout_cached():
|
| """Project + UMAP on the 12,297 held-out Veres slice only."""
|
| cache_p = FIG / "_cache_veres_heldout_marker.npz"
|
| if cache_p.exists():
|
| print(f"[fig6] reusing {cache_p.name}", flush=True)
|
| c = np.load(cache_p, allow_pickle=True)
|
| return c["emb"], c["stage"]
|
|
|
| print("[fig6] loading veres raw + held-out obs list ...", flush=True)
|
| heldout_p = ROOT / "data/corpus/pancreas/held_out_labeled/veres_GSE114412_test.h5ad"
|
| if heldout_p.exists():
|
| heldout_raw = set(ad.read_h5ad(heldout_p).obs_names.astype(str).tolist())
|
| heldout_names = {n[6:] if n.startswith("veres_") else n for n in heldout_raw}
|
| heldout_names |= heldout_raw
|
| else:
|
| print(f"[fig6] WARN: held-out file missing; using ALL veres cells")
|
| heldout_names = None
|
|
|
| a_v = _load_veres()
|
| if heldout_names is not None:
|
| keep = np.array([str(n) in heldout_names for n in a_v.obs_names])
|
| print(f"[fig6] restricting veres to held-out: {keep.sum():,}/{a_v.n_obs:,}", flush=True)
|
| a_v = a_v[keep].copy()
|
|
|
| stage_col = "Stage" if "Stage" in a_v.obs.columns else "stage"
|
| stage = pd.to_numeric(a_v.obs[stage_col], errors="coerce").fillna(-1).astype(int).values
|
| st_str = np.array([str(s) if s > 0 else "islet" for s in stage])
|
| print(f"[fig6] projecting veres held-out ({a_v.n_obs:,}) ...", flush=True)
|
| Z_v, _, _ = project(a_v, "pancreas", "marker")
|
| print(f"[fig6] UMAP veres held-out ...", flush=True)
|
| emb_v = do_umap(Z_v)
|
| np.savez(cache_p, emb=emb_v, stage=st_str)
|
| return emb_v, st_str
|
|
|
|
|
| def fig6_multi():
|
| """3-panel multi-system UMAP colored by biology-of-interest label."""
|
| fig, axes = plt.subplots(1, 3, figsize=(22, 8), constrained_layout=True)
|
|
|
|
|
| emb_s, gt_s = _get_dingwall_cached()
|
| ax = axes[0]
|
| for g in ["other", "WT", "En1-cKO"]:
|
| m = gt_s == g
|
| if m.sum() == 0: continue
|
| ax.scatter(emb_s[m, 0], emb_s[m, 1], s=5, alpha=0.55,
|
| c=GENOTYPE_COLORS.get(g, "#bbbbbb"),
|
| label=f"{g} (n={int(m.sum()):,})",
|
| linewidths=0, rasterized=True)
|
| ax.set_title("(a) Dingwall skin — En1 genotype", fontsize=TITLE_FS, pad=8)
|
| ax.set_xlabel("UMAP-1", fontsize=LABEL_FS)
|
| ax.set_ylabel("UMAP-2", fontsize=LABEL_FS)
|
| ax.set_xticks([]); ax.set_yticks([])
|
| ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.02),
|
| ncol=3, frameon=False, fontsize=LEGEND_FS, markerscale=2.0,
|
| handletextpad=0.4, columnspacing=1.2)
|
|
|
|
|
| emb_d, gt_d = _get_dahlin_cached()
|
| ax = axes[1]
|
|
|
| for g in ["unknown", "WT", "Kit_W41"]:
|
| m = gt_d == g
|
| if m.sum() == 0: continue
|
| c = GENOTYPE_COLORS.get(g, "#bbbbbb") if g != "unknown" else "#bbbbbb"
|
| ax.scatter(emb_d[m, 0], emb_d[m, 1], s=5, alpha=0.55,
|
| c=c, label=f"{g} (n={int(m.sum()):,})",
|
| linewidths=0, rasterized=True)
|
| ax.set_title("(b) Dahlin hematopoiesis — Kit genotype",
|
| fontsize=TITLE_FS, pad=8)
|
| ax.set_xlabel("UMAP-1", fontsize=LABEL_FS)
|
| ax.set_ylabel("UMAP-2", fontsize=LABEL_FS)
|
| ax.set_xticks([]); ax.set_yticks([])
|
| ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.02),
|
| ncol=3, frameon=False, fontsize=LEGEND_FS, markerscale=2.0,
|
| handletextpad=0.4, columnspacing=1.2)
|
|
|
|
|
| emb_v, st_str = _get_veres_heldout_cached()
|
| ax = axes[2]
|
| stage_order = ["islet", "3", "4", "5", "6"]
|
| for s in stage_order:
|
| m = st_str == s
|
| if m.sum() == 0: continue
|
| color = STAGE_COLORS[s] if s in STAGE_COLORS else _ISLET_GRAY
|
| label = (f"Stage {s} (n={int(m.sum()):,})" if s != "islet"
|
| else f"islet (n={int(m.sum()):,})")
|
| ax.scatter(emb_v[m, 0], emb_v[m, 1], s=5, alpha=0.55,
|
| c=color, label=label, linewidths=0, rasterized=True)
|
| ax.set_title(f"(c) Veres pancreas held-out (n={len(st_str):,}) — protocol stage",
|
| fontsize=TITLE_FS, pad=8)
|
| ax.set_xlabel("UMAP-1", fontsize=LABEL_FS)
|
| ax.set_ylabel("UMAP-2", fontsize=LABEL_FS)
|
| ax.set_xticks([]); ax.set_yticks([])
|
| ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.02),
|
| ncol=5, frameon=False, fontsize=LEGEND_FS, markerscale=2.0,
|
| handletextpad=0.4, columnspacing=1.2)
|
|
|
| plt.savefig(FIG / "fig6_multi_umap.pdf", bbox_inches="tight", dpi=180)
|
| plt.close()
|
| print(f"[fig6] wrote {FIG}/fig6_multi_umap.pdf", flush=True)
|
|
|
|
|
| if __name__ == "__main__":
|
| fig5_dingwall()
|
| fig6_multi()
|
|
|