File size: 6,081 Bytes
b67979a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fe43f4f
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
"""gears + ranking baseline for inverse nomination, done entirely in gears' native
gene space (fair: top-k recovery is representation-agnostic). trains gears on our
held-out-perturbation split, predicts post-perturbation expression for every
candidate, and ranks candidates by l2 distance to each held-out target's true mean
expression. emits top-1/5, ndcg@10, median rank for single-gene and combination
targets. runs in the isolated pivot_gears env."""
import os, json, pickle
import numpy as np

PROC = "data/processed/norman"
split = np.load(f"{PROC}/splits/perturbation.npz", allow_pickle=True)
splitC = np.load(f"{PROC}/splits/combination.npz", allow_pickle=True)
my_test = [str(p) for p in split["test_perts"]]
my_train = [str(p) for p in split["train_perts"]]
combo_test = [str(p) for p in splitC["test_perts"]]


def to_gears(p):
    g = [x for x in p.split("_") if x]
    return g[0] + "+ctrl" if len(g) == 1 else g[0] + "+" + g[1]


def ndcg_at_k(ranked, true, k=10):
    rel = [1.0 if r == true else 0.0 for r in ranked[:k]]
    dcg = sum(r / np.log2(i + 2) for i, r in enumerate(rel))
    return float(dcg)  # ideal dcg = 1 (single relevant) -> ndcg = dcg


from gears import PertData, GEARS

dev = "cuda"
pert_data = PertData("./gears_data")
pert_data.load(data_name="norman")
avail = set(map(str, pert_data.adata.obs["condition"].unique()))
gears_test = sorted({to_gears(p) for p in my_test} & avail)
gears_train = sorted(({to_gears(p) for p in my_train} & avail) - set(gears_test))
if "ctrl" in avail:
    gears_train = sorted(set(gears_train) | {"ctrl"})
rng = np.random.default_rng(0)
val = sorted(rng.choice([c for c in gears_train if c != "ctrl"],
                        size=max(1, len(gears_train) // 10), replace=False).tolist())
gears_train = sorted(set(gears_train) - set(val))
split_dict = {"train": gears_train, "val": val, "test": gears_test}
os.makedirs("gears_data", exist_ok=True)
pickle.dump(split_dict, open("gears_data/pivot_custom_split.pkl", "wb"))
pert_data.prepare_split(split="custom", split_dict_path="gears_data/pivot_custom_split.pkl")
pert_data.get_dataloader(batch_size=64, test_batch_size=128)

model = GEARS(pert_data, device=dev)
model.model_initialize(hidden_size=64)
model.train(epochs=20)
print("GEARS trained", flush=True)

adata = pert_data.adata
genes = list(adata.var["gene_name"]) if "gene_name" in adata.var else list(adata.var_names)
cond = adata.obs["condition"].astype(str).values
X = adata.X
X = np.asarray(X.todense()) if hasattr(X, "todense") else np.asarray(X)


def true_mean(gcond):
    m = cond == gcond
    return X[m].mean(0) if m.sum() else None


# candidate single genes present in gears as "GENE+ctrl"
single_conds = sorted([c for c in avail if c.endswith("+ctrl") and c != "ctrl"])
single_genes = [c.split("+")[0] for c in single_conds]
# predict every candidate's post-perturbation expression (gene space)
pred = {}
B = 64
for i in range(0, len(single_genes), B):
    chunk = single_genes[i:i + B]
    try:
        out = model.predict([[g] for g in chunk])
    except Exception as e:
        print("predict chunk failed", chunk[:3], e, flush=True)
        out = {}
    for g in chunk:
        for key in (g, g + "+ctrl", "_".join([g])):
            if key in out:
                pred[g] = np.asarray(out[key]); break
print(f"predicted {len(pred)}/{len(single_genes)} single candidates", flush=True)

cand_genes = [g for g in single_genes if g in pred]
P = np.stack([pred[g] for g in cand_genes])  # [n_cand, n_genes]

# single-gene targets (held-out), matched to pivot singles
single_targets = [t for t in my_test if "_" not in t and to_gears(t).split("+")[0] in pred]
res = {"single": {"top1": [], "top5": [], "ndcg": [], "rank": []}}
for t in single_targets:
    tg = to_gears(t)
    tm = true_mean(tg)
    if tm is None:
        continue
    d = np.linalg.norm(P - tm[None], axis=1)
    order = [cand_genes[i] for i in np.argsort(d)]
    tgene = tg.split("+")[0]
    res["single"]["top1"].append(float(order[0] == tgene))
    res["single"]["top5"].append(float(tgene in order[:5]))
    res["single"]["ndcg"].append(ndcg_at_k(order, tgene, 10))
    res["single"]["rank"].append(int(order.index(tgene) + 1) if tgene in order else len(order))

# combination targets: predict combos and rank among observed combos
combo_conds = sorted([c for c in avail if c.count("+") == 1 and "ctrl" not in c])
cpred = {}
for i in range(0, len(combo_conds), B):
    chunk = combo_conds[i:i + B]
    try:
        out = model.predict([c.split("+") for c in chunk])
    except Exception as e:
        print("combo predict failed", e, flush=True); out = {}
    for c in chunk:
        for key in (c, c.replace("+", "_"), "_".join(c.split("+"))):
            if key in out:
                cpred[c] = np.asarray(out[key]); break
print(f"predicted {len(cpred)}/{len(combo_conds)} combo candidates", flush=True)
cc = [c for c in combo_conds if c in cpred]
if cc:
    PC = np.stack([cpred[c] for c in cc])
    combo_tg = [to_gears(t) for t in combo_test if to_gears(t) in cpred]
    res["combo"] = {"top1": [], "top5": [], "overlap": [], "ndcg": []}
    for tg in combo_tg:
        tm = true_mean(tg)
        if tm is None:
            continue
        d = np.linalg.norm(PC - tm[None], axis=1)
        order = [cc[i] for i in np.argsort(d)]
        res["combo"]["top1"].append(float(order[0] == tg))
        res["combo"]["top5"].append(float(tg in order[:5]))
        res["combo"]["ndcg"].append(ndcg_at_k(order, tg, 10))
        s_true = set(tg.split("+"))
        res["combo"]["overlap"].append(len(set(order[0].split("+")) & s_true) / len(s_true))

agg = {}
for grp, d in res.items():
    agg[grp] = {k: (float(np.mean(v)) if v else None) for k, v in d.items()}
    agg[grp]["n"] = len(next(iter(d.values())))
    if "rank" in d and d["rank"]:
        agg[grp]["med_rank"] = float(np.median(d["rank"]))
agg["n_candidates_single"] = len(cand_genes)
agg["n_candidates_combo"] = len(cc) if cc else 0
json.dump(agg, open("experiments/results/gears_ranking.json", "w"), indent=2, default=float)
print(json.dumps(agg), flush=True)