| """Shared utilities for DeepPTR benchmark scripts. |
| |
| All scripts in analyses/deep/ import from here for reproducibility. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import os |
|
|
| os.environ["OMP_NUM_THREADS"] = "4" |
| os.environ["MKL_NUM_THREADS"] = "4" |
| os.environ["OPENBLAS_NUM_THREADS"] = "4" |
| os.environ["NUMEXPR_NUM_THREADS"] = "4" |
|
|
| import json |
| import sys |
| import time |
| from pathlib import Path |
|
|
| import matplotlib |
|
|
| matplotlib.use("Agg") |
| import matplotlib.pyplot as plt |
| import numpy as np |
| import pandas as pd |
| from scipy import stats |
|
|
| import torch |
|
|
| torch.set_num_threads(4) |
|
|
| |
| def set_figure_style(): |
| plt.rcParams.update({ |
| "figure.dpi": 150, "savefig.dpi": 300, "savefig.bbox": "tight", |
| "font.size": 10, "axes.titlesize": 12, "axes.labelsize": 11, |
| "xtick.labelsize": 9, "ytick.labelsize": 9, "legend.fontsize": 9, |
| "figure.figsize": (6, 5), "axes.spines.top": False, "axes.spines.right": False, |
| }) |
|
|
| import scptr |
|
|
| |
|
|
| PROJECT_ROOT = Path(__file__).parent.parent.parent |
| OUTPUT_ROOT = PROJECT_ROOT / "output" / "deep_benchmarks" |
| DATA_DIR = Path(scptr.benchmark.__file__).parent / "data" |
|
|
| DEEP_HP = dict( |
| d_T=8, d_PT=8, d_hidden=48, n_enc_layers=2, |
| batch_size=512, max_epochs=100, kl_warmup_epochs=20, |
| patience=15, n_posterior_samples=15, |
| device="cpu", seed=0, |
| ) |
|
|
|
|
| def output_dir(script_name: str) -> Path: |
| """Return output directory for a given script, e.g. '01_fair_comparison'.""" |
| d = OUTPUT_ROOT / script_name |
| (d / "figures").mkdir(parents=True, exist_ok=True) |
| (d / "results").mkdir(parents=True, exist_ok=True) |
| return d |
|
|
|
|
| def save_fig(fig, name: str, out: Path, subdir: str = "figures"): |
| if fig is None: |
| return |
| path = out / subdir / f"{name}.png" |
| fig.savefig(path, dpi=150, bbox_inches="tight") |
| plt.close(fig) |
| print(f" Saved: {path}") |
|
|
|
|
| def save_json(data, name: str, out: Path): |
| path = out / "results" / f"{name}.json" |
| with open(path, "w") as f: |
| json.dump(data, f, indent=2, default=str) |
| print(f" Saved: {path}") |
|
|
|
|
| |
|
|
| def load_halflife_refs(): |
| return scptr.datasets.herzog2017_halflives(), scptr.datasets.schofield2018_halflives() |
|
|
|
|
| def select_top_genes(adata, n_top=300): |
| """Select top genes by unspliced signal for DeepPTR.""" |
| from scipy.sparse import issparse |
|
|
| u = adata.layers["unspliced"] |
| if issparse(u): |
| u = np.asarray(u.todense()) |
| u = np.asarray(u, dtype=np.float32) |
| score = u.sum(axis=0) * (u > 0).mean(axis=0) |
| top_idx = np.sort(np.argsort(score)[::-1][:n_top]) |
| adata_sub = adata[:, adata.var_names[top_idx]].copy() |
| from scipy.sparse import issparse as _iss |
|
|
| for key in ("spliced", "unspliced"): |
| if key in adata_sub.layers and _iss(adata_sub.layers[key]): |
| adata_sub.layers[key] = np.asarray(adata_sub.layers[key].todense()) |
| return adata_sub |
|
|
|
|
| def run_analytical(adata_loader): |
| """Run full analytical scPTR pipeline, return adata.""" |
| adata = adata_loader() |
| scptr.pp.filter_genes(adata) |
| scptr.pp.normalize_layers(adata) |
| scptr.pp.neighbors(adata, n_neighbors=30) |
| scptr.pp.smooth_layers(adata) |
| scptr.tl.estimate_beta(adata) |
| scptr.tl.estimate_gamma(adata) |
| return adata |
|
|
|
|
| def run_deep(adata_loader, n_top=300, verbose=True): |
| """Run preprocessing + DeepPTR, return (adata_deep, model, history).""" |
| adata = adata_loader() |
| scptr.pp.filter_genes(adata) |
| scptr.pp.normalize_layers(adata) |
| scptr.pp.neighbors(adata, n_neighbors=30) |
| scptr.pp.smooth_layers(adata) |
| scptr.tl.estimate_beta(adata) |
| adata = select_top_genes(adata, n_top=n_top) |
| torch.set_num_threads(4) |
| model, history = scptr.deep.fit_deepptr(adata, verbose=verbose, **DEEP_HP) |
| return adata, model, history |
|
|
|
|
| def run_both(adata_loader, n_top=300): |
| """Return (adata_analytical, adata_deep, model, history).""" |
| adata_an = run_analytical(adata_loader) |
| adata_dp, model, history = run_deep(adata_loader, n_top=n_top) |
| return adata_an, adata_dp, model, history |
|
|
|
|
| |
|
|
| def match_halflife(adata, hl_df, gene_col="gene_symbol", hl_col="half_life_hours"): |
| """Match genes case-insensitively, return (gamma_vals, hl_vals, gene_names).""" |
| gamma_med = np.median(adata.layers["gamma"], axis=0) |
| hl_s = hl_df.set_index(gene_col)[hl_col] |
|
|
| gamma_upper = {g.upper(): i for i, g in enumerate(adata.var_names)} |
| hl_upper = {g.upper(): g for g in hl_s.index if isinstance(g, str)} |
| shared = set(gamma_upper.keys()) & set(hl_upper.keys()) |
|
|
| idx = [gamma_upper[u] for u in shared] |
| g = gamma_med[idx].astype(float) |
| h = np.array([hl_s[hl_upper[u]] for u in shared], dtype=float) |
| names = [adata.var_names[gamma_upper[u]] for u in shared] |
|
|
| valid = np.isfinite(g) & np.isfinite(h) & (g > 0) & (h > 0) |
| return g[valid], h[valid], [n for n, v in zip(names, valid) if v] |
|
|
|
|
| def halflife_spearman(adata, hl_df): |
| """Quick Spearman r with half-life reference.""" |
| g, h, _ = match_halflife(adata, hl_df) |
| if len(g) < 3: |
| return np.nan, 0 |
| r, _ = stats.spearmanr(g, h) |
| return float(r), len(g) |
|
|
|
|
| |
|
|
| DATASETS = [ |
| ("pancreas", scptr.datasets.pancreas, "clusters"), |
| ("dentate_gyrus", scptr.datasets.dentate_gyrus, "clusters"), |
| ] |
|
|