File size: 7,246 Bytes
4e2940e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#!/usr/bin/env python
"""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()

        # ── scVelo SS ────────────────────────────────────────────────
        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)

        # ── scVelo dynamical ─────────────────────────────────────────
        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)

        # ── scPTR ────────────────────────────────────────────────────
        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)

        # ── Correlations between methods ─────────────────────────────
        print("\n--- Method-to-method gamma correlation ---")

        # scPTR vs scVelo SS
        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()})")

        # scVelo SS vs dynamical fit_gamma
        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()})")

        # ── Corrected half-life comparison ────────────────────────────
        print("\n--- Half-life correlation (corrected) ---")

        methods = {}

        # scPTR gamma
        r_sp, n_sp = halflife_spearman(adata_sp, hl_human)
        methods["scPTR gamma"] = (r_sp, n_sp)

        # scVelo SS velocity_gamma
        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)

        # scVelo dyn fit_gamma (raw β€” the "failed" metric)
        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)

        # scVelo dyn fit_gamma/fit_beta (CORRECTED)
        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()},
        }

        # ── Honest assessment ─────────────────────────────────────────
        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

        # Figure
        fig, axes = plt.subplots(1, 3, figsize=(15, 4.5))

        # Panel 1: scPTR vs scVelo SS scatter
        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")

        # Panel 2: Half-life comparison bar
        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")

        # Panel 3: SS vs dyn scatter
        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()