"""zero-shot PANDA on held-out Baron 2016 mouse pancreas.""" from __future__ import annotations from pathlib import Path import warnings, json, sys, pickle warnings.filterwarnings("ignore") import numpy as np, pandas as pd, anndata as ad, scanpy as sc, scipy.sparse as sp import torch from pathlib import Path as _P_root ROOT = _P_root(__file__).resolve().parents[2] ROOT_STR = str(ROOT) sys.path.insert(0, ROOT_STR) from panda import PANDAEncoder from panda.data.pancreas_loaders import load_baron DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") CKPT = Path(f"{ROOT_STR}/checkpoints/pancreas") HARM = Path(f"{ROOT_STR}/data/corpus/pancreas/harmonized") OUT = Path(f"{ROOT_STR}/discovery/pancreas/marker") OUT.mkdir(parents=True, exist_ok=True) def project(a, shared_hvgs, mu, sig): G = len(shared_hvgs); n = a.n_obs hvg2i = {g: i for i, g in enumerate(shared_hvgs)} common = [g for g in a.var_names.astype(str) if g in hvg2i] present_frac = len(common) / G print(f"[proj] {len(common)}/{G} HVGs present ({present_frac:.1%})", flush=True) a_c = a[:, common].copy() sc.pp.normalize_total(a_c, target_sum=1e4) sc.pp.log1p(a_c) X = a_c.X.toarray().astype(np.float32) if sp.issparse(a_c.X) else a_c.X.astype(np.float32) Xf = np.zeros((n, G), dtype=np.float32) cols = [hvg2i[g] for g in common] Xf[:, cols] = X Xz = np.clip((Xf - mu.astype(np.float32)) / sig.astype(np.float32), -10, 10) return Xz, present_frac def main(): ck = torch.load(CKPT / "panda_final.pt", map_location=DEVICE, weights_only=False) classes = ck["classes"]; datasets = ck["datasets"] model = PANDAEncoder(n_pca=50, n_classes=len(classes), n_datasets=len(datasets)).to(DEVICE).eval() model.load_state_dict(ck["model"]) print(f"[model] classes: {classes}", flush=True) stats = np.load(HARM / "corpus_stats.npz", allow_pickle=True) shared_hvgs = [str(g) for g in stats["shared_hvgs"]] mu, sig = stats["mean"], stats["std"] with open(HARM / "pca_basis.pkl", "rb") as f: pca = pickle.load(f) a = load_baron() print(f"[target] Baron shape: {a.shape}", flush=True) print(f"[target] labels (assigned_cluster head): " f"{a.obs.get('assigned_cluster', pd.Series(dtype=str)).value_counts().head(12).to_dict()}", flush=True) Xz, present_frac = project(a, shared_hvgs, mu, sig) counts = np.asarray(a.X.sum(axis=1)).ravel() logc = np.log10(counts + 1); logc = (logc - logc.mean()) / (logc.std() + 1e-6) Xpca = pca.transform(Xz).astype(np.float32) mhf = np.full(a.n_obs, 1.0 - present_frac, dtype=np.float32) all_z, batch = [], 4096 with torch.no_grad(): for i in range(0, a.n_obs, batch): xb = torch.from_numpy(Xpca[i:i+batch]).to(DEVICE) aux = torch.from_numpy(np.stack([mhf[i:i+batch], logc[i:i+batch]], 1).astype(np.float32)).to(DEVICE) out = model(xb, aux, lam_dann=0.0) all_z.append(out["z"].cpu().numpy()) Z = np.concatenate(all_z, axis=0) protos = ck["prototypes"] protos = protos / (np.linalg.norm(protos, axis=1, keepdims=True) + 1e-8) cos = Z @ protos.T pred_ix = cos.argmax(axis=1) pred_label = np.array([classes[i] for i in pred_ix], dtype=object) conf = cos.max(axis=1) a.obs["pred_label"] = pred_label a.obs["pred_conf"] = conf.astype(np.float32) a.obs.to_csv(OUT / "baron_predictions.csv") print(f"\n[summary] Baron pred_label breakdown:") print(pd.Series(pred_label).value_counts()) if "assigned_cluster" in a.obs.columns: norm_map = { "alpha": "alpha", "beta": "beta", "delta": "delta", "gamma": "gamma", "epsilon": "epsilon", "ductal": "ductal", "acinar": "acinar", "endothelial": "endothelial", "activated_stellate": "other", "quiescent_stellate": "other", "schwann": "other", "mast": "immune", "macrophage": "immune", "t_cell": "immune", "T_cell": "immune", } true_lbl = a.obs["assigned_cluster"].astype(str).str.lower().map(norm_map).fillna("other") mask = true_lbl.isin(classes) if mask.sum() > 0: from sklearn.metrics import classification_report, accuracy_score true = true_lbl[mask].values pred = pred_label[mask.values] print(f"\n[eval] shared-class accuracy: {accuracy_score(true, pred):.4f} on {mask.sum()} cells") print(classification_report(true, pred, digits=3, zero_division=0)) with open(OUT / "baron_accuracy.json", "w") as f: json.dump({"acc": accuracy_score(true, pred), "n_evaluated": int(mask.sum())}, f, indent=2) else: print("[eval] no shared classes; skipping") if __name__ == "__main__": main()