| |
| """CRITICAL: scVelo gamma semantics and honest method comparison. |
| |
| Findings: |
| 1. scPTR gamma β scVelo velocity_gamma (r=0.96) β nearly identical math |
| 2. scVelo dynamical fit_gamma has DIFFERENT semantics (r=-0.37 with SS) |
| 3. fit_gamma/fit_beta correlates with half-life (r=-0.35) |
| 4. The "scVelo dynamical failure" was a comparison error, not a model failure |
| |
| This script documents these findings honestly and provides corrected comparisons. |
| """ |
| from _common import * |
| import scvelo as scv |
|
|
| OUT = output_dir("28_gamma_semantics") |
|
|
|
|
| def main(): |
| set_figure_style() |
| hl_mouse, hl_human = load_halflife_refs() |
|
|
| all_results = {} |
|
|
| for ds_name, loader, ck in DATASETS: |
| print(f"\n{'=' * 60}\n{ds_name.upper()}: Gamma Semantics\n{'=' * 60}") |
|
|
| adata_raw = loader() |
|
|
| |
| adata_sv = adata_raw.copy() |
| scv.pp.filter_and_normalize(adata_sv, min_shared_counts=20, n_top_genes=2000) |
| scv.pp.moments(adata_sv, n_pcs=30, n_neighbors=30) |
| scv.tl.velocity(adata_sv, mode="steady_state") |
| vg = adata_sv.var["velocity_gamma"].values.astype(float) |
|
|
| |
| adata_dyn = adata_raw.copy() |
| scv.pp.filter_and_normalize(adata_dyn, min_shared_counts=20, n_top_genes=2000) |
| scv.pp.moments(adata_dyn, n_pcs=30, n_neighbors=30) |
| scv.tl.recover_dynamics(adata_dyn, n_jobs=4) |
| scv.tl.velocity(adata_dyn, mode="dynamical") |
| fg = adata_dyn.var["fit_gamma"].values.astype(float) |
| fb = adata_dyn.var["fit_beta"].values.astype(float) |
|
|
| |
| adata_sp = run_analytical(loader) |
| sp_gamma = np.median(adata_sp.layers["gamma"], axis=0) |
| sp_gs = pd.Series(sp_gamma, index=adata_sp.var_names) |
|
|
| |
| print("\n--- Method-to-method gamma correlation ---") |
|
|
| |
| sv_gs = pd.Series(vg, index=adata_sv.var_names) |
| shared_ss = sp_gs.index.intersection(sv_gs.index) |
| g1, g2 = sp_gs[shared_ss].values.astype(float), sv_gs[shared_ss].values.astype(float) |
| v = np.isfinite(g1) & np.isfinite(g2) & (g1 > 0) & (g2 > 0) |
| r_sp_sv, _ = stats.spearmanr(g1[v], g2[v]) |
| print(f" scPTR vs scVelo SS: r={r_sp_sv:.4f} (n={v.sum()})") |
|
|
| |
| shared_dyn = adata_sv.var_names.intersection(adata_dyn.var_names) |
| vg_sh = pd.Series(vg, index=adata_sv.var_names)[shared_dyn].values.astype(float) |
| fg_sh = pd.Series(fg, index=adata_dyn.var_names)[shared_dyn].values.astype(float) |
| v2 = np.isfinite(vg_sh) & np.isfinite(fg_sh) & (vg_sh > 0) & (fg_sh > 0) |
| r_ss_dyn, _ = stats.spearmanr(vg_sh[v2], fg_sh[v2]) |
| print(f" scVelo SS vs dyn fit_gamma: r={r_ss_dyn:.4f} (n={v2.sum()})") |
|
|
| |
| print("\n--- Half-life correlation (corrected) ---") |
|
|
| methods = {} |
|
|
| |
| r_sp, n_sp = halflife_spearman(adata_sp, hl_human) |
| methods["scPTR gamma"] = (r_sp, n_sp) |
|
|
| |
| adata_sv_tmp = adata_sv.copy() |
| adata_sv_tmp.layers["gamma"] = np.tile(vg, (adata_sv.n_obs, 1)) |
| r_ss, n_ss = halflife_spearman(adata_sv_tmp, hl_human) |
| methods["scVelo SS velocity_gamma"] = (r_ss, n_ss) |
|
|
| |
| adata_dyn_tmp = adata_dyn.copy() |
| adata_dyn_tmp.layers["gamma"] = np.tile(fg, (adata_dyn.n_obs, 1)) |
| r_dyn_raw, n_dyn_raw = halflife_spearman(adata_dyn_tmp, hl_human) |
| methods["scVelo dyn fit_gamma (raw)"] = (r_dyn_raw, n_dyn_raw) |
|
|
| |
| ratio = fg / (fb + 1e-8) |
| adata_dyn_tmp.layers["gamma"] = np.tile(ratio, (adata_dyn.n_obs, 1)) |
| r_dyn_corr, n_dyn_corr = halflife_spearman(adata_dyn_tmp, hl_human) |
| methods["scVelo dyn fit_gamma/fit_beta"] = (r_dyn_corr, n_dyn_corr) |
|
|
| for mname, (r, n) in methods.items(): |
| print(f" {mname:<35} r={r:.4f} (n={n})") |
|
|
| ds_results = { |
| "scptr_vs_scvelo_ss": {"r": float(r_sp_sv), "n": int(v.sum())}, |
| "scvelo_ss_vs_dyn": {"r": float(r_ss_dyn), "n": int(v2.sum())}, |
| "halflife": {m: {"r": float(r), "n": n} for m, (r, n) in methods.items()}, |
| } |
|
|
| |
| print(f"\n--- Honest assessment ---") |
| print(f" scPTR gamma β scVelo SS gamma (r={r_sp_sv:.3f})") |
| print(f" scVelo dynamical fit_gamma has different semantics") |
| print(f" Corrected (fit_gamma/fit_beta): r={r_dyn_corr:.3f} β comparable to SS") |
| print(f" scPTR's methodological contribution over scVelo SS:") |
| print(f" 1. Per-cell gamma (not just per-gene)") |
| print(f" 2. Beta estimation + multiplication") |
| print(f" 3. Two-stage clipping (per-gene + global)") |
| print(f" 4. Downstream: PT states, PT velocity, networks") |
| print(f" 5. DeepPTR: uncertainty + disentanglement") |
|
|
| all_results[ds_name] = ds_results |
|
|
| |
| fig, axes = plt.subplots(1, 3, figsize=(15, 4.5)) |
|
|
| |
| axes[0].scatter(g2[v], g1[v], alpha=0.1, s=3, c="steelblue") |
| axes[0].set_xlabel("scVelo SS velocity_gamma") |
| axes[0].set_ylabel("scPTR median gamma") |
| axes[0].set_title(f"scPTR β scVelo SS (r={r_sp_sv:.3f})") |
| axes[0].set_xscale("log"); axes[0].set_yscale("log") |
|
|
| |
| mnames = list(methods.keys()) |
| rs = [abs(methods[m][0]) for m in mnames] |
| colors = ["darkorange", "steelblue", "lightcoral", "seagreen"] |
| axes[1].barh(mnames, rs, color=colors[:len(mnames)], alpha=0.7) |
| axes[1].set_xlabel("|Spearman r| with half-life") |
| axes[1].set_title(f"{ds_name}: Corrected comparison") |
|
|
| |
| axes[2].scatter(vg_sh[v2], fg_sh[v2], alpha=0.1, s=3, c="gray") |
| axes[2].set_xlabel("scVelo SS velocity_gamma") |
| axes[2].set_ylabel("scVelo dyn fit_gamma") |
| axes[2].set_title(f"SS vs dyn gamma (r={r_ss_dyn:.3f})") |
| axes[2].set_xscale("log"); axes[2].set_yscale("log") |
|
|
| fig.suptitle(f"{ds_name}: Gamma semantics", y=1.02) |
| fig.tight_layout() |
| save_fig(fig, f"{ds_name}_gamma_semantics", OUT) |
|
|
| save_json(all_results, "gamma_semantics", OUT) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|